-
-
Save Dan21097/d548394c5a6dd000247f54c42f858579 to your computer and use it in GitHub Desktop.
for gpt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ВИЗУАЛИЗАТОР ФРАКТАЛОВ - ИСПРАВЛЕННЫЙ ПОЛНОЭКРАННЫЙ РЕЖИМ И СОХРАНЕНИЕ ПРОПОРЦИЙ""" | |
| import pygame | |
| import numpy as np | |
| import sys | |
| import time | |
| from numba import njit, prange | |
| import random | |
| import math | |
| # --- Инициализация Pygame --- | |
| pygame.init() | |
| # --- Изначальные размеры (для оконного режима) --- | |
| INIT_WIDTH, INIT_HEIGHT = 1000, 700 | |
| MENU_HEIGHT = 50 | |
| # Переменные для текущих размеров (будут меняться) | |
| WIDTH, HEIGHT = INIT_WIDTH, INIT_HEIGHT | |
| FRACTAL_HEIGHT = HEIGHT - MENU_HEIGHT | |
| COLOR_MAP = np.array([ | |
| [0, 0, 0], [25, 7, 26], [9, 1, 47], [4, 4, 73], [0, 7, 100], | |
| [12, 44, 138], [24, 82, 177], [57, 125, 209], [134, 181, 229], | |
| [211, 236, 248], [241, 233, 191], [248, 201, 95], [255, 170, 0], | |
| [204, 128, 0], [153, 87, 0], [106, 52, 3] | |
| ], dtype=np.uint8) | |
| # === Оптимизированные вычислительные функции === | |
| @njit(parallel=True, fastmath=True, cache=True, nogil=True) | |
| def compute_mandelbrot_ultra(x_min, x_max, y_min, y_max, width, height, max_iter): | |
| """Ультра-оптимизированный Мандельброт""" | |
| result = np.empty((height, width), dtype=np.int32) | |
| dx = (x_max - x_min) / width | |
| dy = (y_max - y_min) / height | |
| for y in prange(height): | |
| c_imag = y_min + y * dy | |
| for x in prange(width): | |
| c_real = x_min + x * dx | |
| z_real = 0.0 | |
| z_imag = 0.0 | |
| z_real2 = 0.0 | |
| z_imag2 = 0.0 | |
| for i in range(max_iter): | |
| z_imag = 2.0 * z_real * z_imag + c_imag | |
| z_real = z_real2 - z_imag2 + c_real | |
| z_real2 = z_real * z_real | |
| z_imag2 = z_imag * z_imag | |
| if z_real2 + z_imag2 > 4.0: | |
| result[y, x] = i | |
| break | |
| else: | |
| result[y, x] = max_iter | |
| return result | |
| @njit(parallel=True, fastmath=True, cache=True, nogil=True) | |
| def compute_julia_ultra(x_min, x_max, y_min, y_max, width, height, max_iter, c_real, c_imag): | |
| """Ультра-оптимизированная Жюлиа""" | |
| result = np.empty((height, width), dtype=np.int32) | |
| dx = (x_max - x_min) / width | |
| dy = (y_max - y_min) / height | |
| for y in prange(height): | |
| z_imag = y_min + y * dy | |
| for x in prange(width): | |
| z_real = x_min + x * dx | |
| zr = z_real | |
| zi = z_imag | |
| zr2 = z_real * z_real | |
| zi2 = z_imag * z_imag | |
| for i in range(max_iter): | |
| zi = 2.0 * zr * zi + c_imag | |
| zr = zr2 - zi2 + c_real | |
| zr2 = zr * zr | |
| zi2 = zi * zi | |
| if zr2 + zi2 > 4.0: | |
| result[y, x] = i | |
| break | |
| else: | |
| result[y, x] = max_iter | |
| return result | |
| @njit(fastmath=True, cache=True) | |
| def apply_color_map_fast(iterations, max_iter, color_map, output): | |
| """Быстрое применение цветовой карты""" | |
| height, width = iterations.shape | |
| cmap_size = len(color_map) | |
| for y in range(height): | |
| for x in range(width): | |
| it = iterations[y, x] | |
| if it == max_iter: | |
| idx = 0 | |
| else: | |
| idx = (it * (cmap_size - 1)) // max_iter + 1 | |
| if idx >= cmap_size: | |
| idx = cmap_size - 1 | |
| color = color_map[idx] | |
| output[x, y, 0] = color[0] | |
| output[x, y, 1] = color[1] | |
| output[x, y, 2] = color[2] | |
| # === Класс рендерера === | |
| class FractalRenderer: | |
| def __init__(self): | |
| self.width = WIDTH | |
| self.height = FRACTAL_HEIGHT | |
| self.color_map = COLOR_MAP | |
| self.is_rendering = False | |
| self.should_stop = False | |
| # Предвыделенные буферы (для скорости) | |
| self.iter_buffer = None | |
| self.rgb_buffer = None | |
| self._update_buffers() | |
| def _update_buffers(self): | |
| """Обновление буферов при изменении размера""" | |
| self.width = WIDTH | |
| self.height = FRACTAL_HEIGHT | |
| self.iter_buffer = np.empty((self.height, self.width), dtype=np.int32) | |
| self.rgb_buffer = np.empty((self.width, self.height, 3), dtype=np.uint8) | |
| def render_mandelbrot(self, x_min, x_max, y_min, y_max, max_iter): | |
| self.is_rendering = True | |
| self.should_stop = False | |
| # Обновляем буферы, если размер изменился | |
| if self.iter_buffer.shape != (FRACTAL_HEIGHT, WIDTH): | |
| self._update_buffers() | |
| # ВЫЧИСЛЯЕМ ПРАВИЛЬНОЕ СООТНОШЕНИЕ СТОРОН | |
| aspect_ratio = (x_max - x_min) / (y_max - y_min) | |
| target_aspect = WIDTH / FRACTAL_HEIGHT | |
| # Корректируем область просмотра для сохранения пропорций | |
| if aspect_ratio > target_aspect: | |
| # Область слишком широкая - корректируем по высоте | |
| height_adjusted = (x_max - x_min) / target_aspect | |
| y_center = (y_min + y_max) / 2 | |
| y_min = y_center - height_adjusted / 2 | |
| y_max = y_center + height_adjusted / 2 | |
| else: | |
| # Область слишком высокая - корректируем по ширине | |
| width_adjusted = (y_max - y_min) * target_aspect | |
| x_center = (x_min + x_max) / 2 | |
| x_min = x_center - width_adjusted / 2 | |
| x_max = x_center + width_adjusted / 2 | |
| # Вычисление итераций | |
| self.iter_buffer = compute_mandelbrot_ultra( | |
| x_min, x_max, y_min, y_max, | |
| self.width, self.height, max_iter | |
| ) | |
| # Применение цветов | |
| apply_color_map_fast(self.iter_buffer, max_iter, self.color_map, self.rgb_buffer) | |
| # Создание поверхности | |
| surface = pygame.Surface((self.width, self.height)) | |
| pygame.surfarray.blit_array(surface, self.rgb_buffer) | |
| self.is_rendering = False | |
| return surface, (x_min, x_max, y_min, y_max) | |
| def render_julia(self, x_min, x_max, y_min, y_max, max_iter, c_real, c_imag): | |
| self.is_rendering = True | |
| self.should_stop = False | |
| # Обновляем буферы, если размер изменился | |
| if self.iter_buffer.shape != (FRACTAL_HEIGHT, WIDTH): | |
| self._update_buffers() | |
| # ВЫЧИСЛЯЕМ ПРАВИЛЬНОЕ СООТНОШЕНИЕ СТОРОН | |
| aspect_ratio = (x_max - x_min) / (y_max - y_min) | |
| target_aspect = WIDTH / FRACTAL_HEIGHT | |
| # Корректируем область просмотра для сохранения пропорций | |
| if aspect_ratio > target_aspect: | |
| # Область слишком широкая - корректируем по высоте | |
| height_adjusted = (x_max - x_min) / target_aspect | |
| y_center = (y_min + y_max) / 2 | |
| y_min = y_center - height_adjusted / 2 | |
| y_max = y_center + height_adjusted / 2 | |
| else: | |
| # Область слишком высокая - корректируем по ширине | |
| width_adjusted = (y_max - y_min) * target_aspect | |
| x_center = (x_min + x_max) / 2 | |
| x_min = x_center - width_adjusted / 2 | |
| x_max = x_center + width_adjusted / 2 | |
| # Вычисление итераций | |
| self.iter_buffer = compute_julia_ultra( | |
| x_min, x_max, y_min, y_max, | |
| self.width, self.height, max_iter, | |
| c_real, c_imag | |
| ) | |
| # Применение цветов | |
| apply_color_map_fast(self.iter_buffer, max_iter, self.color_map, self.rgb_buffer) | |
| # Создание поверхности | |
| surface = pygame.Surface((self.width, self.height)) | |
| pygame.surfarray.blit_array(surface, self.rgb_buffer) | |
| self.is_rendering = False | |
| return surface, (x_min, x_max, y_min, y_max) | |
| def stop_rendering(self): | |
| self.should_stop = True | |
| # === Классы интерфейса === | |
| class UIButton: | |
| def __init__(self, x, y, w, h, text, color=(70, 70, 70), hover_color=(100, 100, 100)): | |
| self.rect = pygame.Rect(x, y, w, h) | |
| self.text = text | |
| self.color = color | |
| self.hover_color = hover_color | |
| self.current_color = color | |
| self.font = pygame.font.SysFont('Arial', 20) | |
| self.is_hovered = False | |
| def draw(self, surface): | |
| pygame.draw.rect(surface, self.current_color, self.rect, border_radius=5) | |
| pygame.draw.rect(surface, (40, 40, 40), self.rect, 2, border_radius=5) | |
| text_surf = self.font.render(self.text, True, (255, 255, 255)) | |
| text_rect = text_surf.get_rect(center=self.rect.center) | |
| surface.blit(text_surf, text_rect) | |
| def check_hover(self, pos): | |
| self.is_hovered = self.rect.collidepoint(pos) | |
| self.current_color = self.hover_color if self.is_hovered else self.color | |
| return self.is_hovered | |
| def is_clicked(self, pos, event): | |
| if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: | |
| return self.rect.collidepoint(pos) | |
| return False | |
| def update_position(self, x, y, w=None, h=None): | |
| """Обновление позиции и размера кнопки""" | |
| if w is not None: | |
| self.rect.w = w | |
| if h is not None: | |
| self.rect.h = h | |
| self.rect.x = x | |
| self.rect.y = y | |
| class IterationInputBox: | |
| def __init__(self, x, y, w, h, initial_value=100): | |
| self.rect = pygame.Rect(x, y, w, h) | |
| self.value = initial_value | |
| self.text = str(initial_value) | |
| self.font = pygame.font.SysFont('Arial', 18) | |
| self.active = False | |
| self.color_inactive = (100, 100, 100) | |
| self.color_active = (150, 150, 150) | |
| self.color = self.color_inactive | |
| self.last_click_time = 0 | |
| self.numpad_map = { | |
| pygame.K_KP0: '0', pygame.K_KP1: '1', pygame.K_KP2: '2', | |
| pygame.K_KP3: '3', pygame.K_KP4: '4', pygame.K_KP5: '5', | |
| pygame.K_KP6: '6', pygame.K_KP7: '7', pygame.K_KP8: '8', | |
| pygame.K_KP9: '9' | |
| } | |
| def handle_event(self, event): | |
| if event.type == pygame.MOUSEBUTTONDOWN: | |
| if self.rect.collidepoint(event.pos): | |
| self.active = True | |
| self.color = self.color_active | |
| self.last_click_time = time.time() | |
| else: | |
| self.active = False | |
| self.color = self.color_inactive | |
| if event.type == pygame.KEYDOWN and self.active: | |
| if event.key == pygame.K_RETURN or event.key == pygame.K_KP_ENTER: | |
| self.active = False | |
| self.color = self.color_inactive | |
| try: | |
| new_value = int(self.text) | |
| self.value = max(10, min(10000, new_value)) | |
| self.text = str(self.value) | |
| return self.value | |
| except: | |
| self.text = str(self.value) | |
| return None | |
| elif event.key == pygame.K_BACKSPACE: | |
| self.text = self.text[:-1] | |
| else: | |
| if event.unicode and event.unicode.isdigit(): | |
| if len(self.text) < 6: | |
| self.text += event.unicode | |
| elif event.key in self.numpad_map: | |
| if len(self.text) < 6: | |
| self.text += self.numpad_map[event.key] | |
| return None | |
| def draw(self, screen): | |
| pygame.draw.rect(screen, self.color, self.rect, 2, border_radius=3) | |
| pygame.draw.rect(screen, (30, 30, 30), self.rect) | |
| text_surface = self.font.render(self.text, True, (255, 255, 255)) | |
| screen.blit(text_surface, (self.rect.x + 5, self.rect.y + 5)) | |
| if self.active and int(time.time() * 2) % 2 == 0: | |
| cursor_x = self.rect.x + 5 + text_surface.get_width() | |
| pygame.draw.line(screen, (255, 255, 255), | |
| (cursor_x, self.rect.y + 5), | |
| (cursor_x, self.rect.y + self.rect.height - 5), 2) | |
| def set_value(self, value): | |
| self.value = value | |
| self.text = str(value) | |
| def update_position(self, x, y, w=None, h=None): | |
| """Обновление позиции""" | |
| if w is not None: | |
| self.rect.w = w | |
| if h is not None: | |
| self.rect.h = h | |
| self.rect.x = x | |
| self.rect.y = y | |
| class SmoothSlider: | |
| def __init__(self, x, y, w, h, min_val, max_val, initial_val, label): | |
| self.rect = pygame.Rect(x, y, w, h) | |
| self.min_val = min_val | |
| self.max_val = max_val | |
| self.value = initial_val | |
| self.label = label | |
| self.dragging = False | |
| self.font = pygame.font.SysFont('Arial', 14) | |
| self.knob_radius = 8 | |
| self.knob_x = x + int((initial_val - min_val) / (max_val - min_val) * w) | |
| self.knob_rect = pygame.Rect(self.knob_x - self.knob_radius, | |
| y + h//2 - self.knob_radius, | |
| self.knob_radius * 2, self.knob_radius * 2) | |
| # Для плавности: уменьшаем чувствительность | |
| self.sensitivity = 0.5 # В 2 раза менее чувствительный | |
| def handle_event(self, event): | |
| if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: | |
| if self.knob_rect.collidepoint(event.pos): | |
| self.dragging = True | |
| return True | |
| elif self.rect.collidepoint(event.pos): | |
| # Клик по треку - перемещаем ползунок с уменьшенной чувствительностью | |
| self.knob_x = event.pos[0] | |
| self.knob_x = max(self.rect.left, min(self.rect.right, self.knob_x)) | |
| # Уменьшаем изменение значения | |
| rel_pos = (self.knob_x - self.rect.left) / self.rect.width | |
| self.value = self.min_val + rel_pos * (self.max_val - self.min_val) * self.sensitivity | |
| # Корректируем положение ползунка | |
| adjusted_rel_pos = (self.value - self.min_val) / ((self.max_val - self.min_val) * self.sensitivity) | |
| self.knob_x = self.rect.left + adjusted_rel_pos * self.rect.width | |
| self.knob_rect.centerx = self.knob_x | |
| self.knob_rect.centery = self.rect.centery | |
| return True | |
| elif event.type == pygame.MOUSEBUTTONUP and event.button == 1: | |
| self.dragging = False | |
| return False | |
| elif event.type == pygame.MOUSEMOTION and self.dragging: | |
| self.knob_x = event.pos[0] | |
| self.knob_x = max(self.rect.left, min(self.rect.right, self.knob_x)) | |
| # Уменьшаем изменение значения | |
| rel_pos = (self.knob_x - self.rect.left) / self.rect.width | |
| self.value = self.min_val + rel_pos * (self.max_val - self.min_val) * self.sensitivity | |
| # Корректируем положение ползунка | |
| adjusted_rel_pos = (self.value - self.min_val) / ((self.max_val - self.min_val) * self.sensitivity) | |
| self.knob_x = self.rect.left + adjusted_rel_pos * self.rect.width | |
| self.knob_rect.centerx = self.knob_x | |
| self.knob_rect.centery = self.rect.centery | |
| return True | |
| return False | |
| def draw(self, screen): | |
| # Трек ползунка | |
| pygame.draw.rect(screen, (60, 60, 60), self.rect, border_radius=3) | |
| pygame.draw.rect(screen, (80, 80, 80), self.rect, 1, border_radius=3) | |
| # Ползунок | |
| pygame.draw.circle(screen, (120, 180, 220), | |
| (self.knob_x, self.rect.centery), | |
| self.knob_radius) | |
| pygame.draw.circle(screen, (40, 40, 40), | |
| (self.knob_x, self.rect.centery), | |
| self.knob_radius, 1) | |
| # Текст | |
| label_text = self.font.render(f"{self.label}: {self.value:.3f}", True, (220, 220, 220)) | |
| screen.blit(label_text, (self.rect.x, self.rect.y - 20)) | |
| def update_position(self, x, y, w=None, h=None): | |
| """Обновление позиции слайдера""" | |
| if w is not None: | |
| self.rect.w = w | |
| if h is not None: | |
| self.rect.h = h | |
| self.rect.x = x | |
| self.rect.y = y | |
| # Пересчитываем положение ползунка | |
| self.knob_x = x + int((self.value - self.min_val) / (self.max_val - self.min_val) * self.rect.width) | |
| self.knob_rect.centerx = self.knob_x | |
| self.knob_rect.centery = self.rect.centery | |
| # === Главное меню === | |
| class MainMenu: | |
| def __init__(self, app): | |
| self.app = app | |
| self._init_ui() | |
| self.title_font = pygame.font.SysFont('Arial', 48, bold=True) | |
| self.sub_font = pygame.font.SysFont('Arial', 24) | |
| def _init_ui(self): | |
| """Инициализация UI элементов с текущими размерами""" | |
| btn_w, btn_h = 300, 70 | |
| center_x = WIDTH // 2 | |
| self.mandel_btn = UIButton(center_x - btn_w//2, HEIGHT//2 - 100, btn_w, btn_h, | |
| "ФРАКТАЛ МАНДЕЛЬБРОТА", (80, 120, 200), (100, 140, 220)) | |
| self.julia_btn = UIButton(center_x - btn_w//2, HEIGHT//2, btn_w, btn_h, | |
| "ФРАКТАЛ ЖЮЛИА", (200, 120, 80), (220, 140, 100)) | |
| self.exit_btn = UIButton(WIDTH - 150, HEIGHT - 70, 120, 50, | |
| "ВЫХОД", (180, 60, 60), (200, 80, 80)) | |
| self.buttons = [self.mandel_btn, self.julia_btn, self.exit_btn] | |
| def update_ui(self): | |
| """Обновление UI при изменении размеров окна""" | |
| self._init_ui() | |
| def handle_event(self, event): | |
| if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: | |
| pos = event.pos | |
| if self.mandel_btn.rect.collidepoint(pos): | |
| return 'mandelbrot' | |
| elif self.julia_btn.rect.collidepoint(pos): | |
| return 'julia' | |
| elif self.exit_btn.rect.collidepoint(pos): | |
| return 'quit' | |
| elif event.type == pygame.KEYDOWN: | |
| if event.key == pygame.K_1 or event.key == pygame.K_m: | |
| return 'mandelbrot' | |
| elif event.key == pygame.K_2 or event.key == pygame.K_j: | |
| return 'julia' | |
| elif event.key == pygame.K_ESCAPE or event.key == pygame.K_q: | |
| return 'quit' | |
| elif event.key == pygame.K_F11: | |
| self.app.toggle_fullscreen() | |
| return None | |
| def draw(self, screen): | |
| screen.fill((30, 30, 40)) | |
| title = self.title_font.render("ВИЗУАЛИЗАТОР ФРАКТАЛОВ", True, (255, 255, 255)) | |
| subtitle = self.sub_font.render("Выберите тип фрактала", True, (200, 200, 220)) | |
| screen.blit(title, (WIDTH//2 - title.get_width()//2, 100)) | |
| screen.blit(subtitle, (WIDTH//2 - subtitle.get_width()//2, 160)) | |
| for btn in self.buttons: | |
| btn.draw(screen) | |
| font = pygame.font.SysFont('Arial', 18) | |
| hints = [ | |
| "Управление:", | |
| "• ЛКМ + перемещение — передвижение", | |
| "• Колёсико мыши — масштабирование", | |
| "• ESC — меню, R — сброс", | |
| "• +/- — итерации, C — случайный", | |
| "• F11 — полноэкранный режим" | |
| ] | |
| hint_x = 50 | |
| hint_y = HEIGHT - 180 | |
| for i, hint in enumerate(hints): | |
| text = font.render(hint, True, (180, 180, 200)) | |
| screen.blit(text, (hint_x, hint_y + i * 25)) | |
| # === Навигация === | |
| class Navigation: | |
| def __init__(self): | |
| self.mode = "idle" | |
| self.start_pos = (0, 0) | |
| self.current_pos = (0, 0) | |
| def start_pan(self, pos): | |
| if self.mode == "idle": | |
| self.mode = "panning" | |
| self.start_pos = pos | |
| pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_SIZEALL) | |
| def update_pan(self, pos): | |
| if self.mode == "panning": | |
| self.current_pos = pos | |
| def end_pan(self): | |
| if self.mode == "panning": | |
| self.mode = "idle" | |
| pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_ARROW) | |
| dx = self.current_pos[0] - self.start_pos[0] | |
| dy = self.current_pos[1] - self.start_pos[1] | |
| return (dx, dy) | |
| return (0, 0) | |
| # === FPS-счётчик === | |
| class FPScounter: | |
| def __init__(self): | |
| self.clock = pygame.time.Clock() | |
| self.frame_count = 0 | |
| self.fps = 0 | |
| self.last_time = time.time() | |
| def update(self): | |
| self.frame_count += 1 | |
| current_time = time.time() | |
| if current_time - self.last_time >= 0.5: | |
| self.fps = self.frame_count * 2 | |
| self.frame_count = 0 | |
| self.last_time = current_time | |
| self.clock.tick() | |
| # === Окно фрактала === | |
| class FractalViewer: | |
| def __init__(self, app, fractal_type): | |
| self.app = app | |
| self.fractal_type = fractal_type | |
| self.renderer = FractalRenderer() | |
| self.nav = Navigation() | |
| self.fps_counter = FPScounter() | |
| if fractal_type == 'mandelbrot': | |
| self.viewport = {'x_min': -2.5, 'x_max': 1.0, | |
| 'y_min': -1.0, 'y_max': 1.0, | |
| 'max_iter': 100} | |
| else: | |
| self.viewport = {'x_min': -2.0, 'x_max': 2.0, | |
| 'y_min': -1.5, 'y_max': 1.5, | |
| 'max_iter': 100, | |
| 'c': complex(-0.7, 0.27015)} | |
| self._init_ui() | |
| self.current_surface = None | |
| self.actual_viewport = None # Фактический viewport после коррекции пропорций | |
| self.needs_render = True | |
| self.preview_offset = (0, 0) | |
| self._render_fractal() | |
| def _init_ui(self): | |
| """Инициализация UI элементов с текущими размерами""" | |
| self.back_btn = UIButton(10, HEIGHT - 40, 100, 30, "← МЕНЮ") | |
| self.reset_btn = UIButton(120, HEIGHT - 40, 100, 30, "СБРОС") | |
| self.iter_label = pygame.font.SysFont('Arial', 14).render("Итерации:", True, (220, 220, 220)) | |
| self.iter_label_pos = (230, HEIGHT - 35) | |
| self.iter_input = IterationInputBox(300, HEIGHT - 40, 60, 30, 100) | |
| self.real_slider = None | |
| self.imag_slider = None | |
| if self.fractal_type == 'julia': | |
| # Автоматический расчет позиции ползунков | |
| available_width = WIDTH - 370 - 20 # Ширина доступного пространства | |
| slider_width = min(200, available_width // 2 - 10) | |
| self.real_slider = SmoothSlider(370, HEIGHT - 35, slider_width, 10, -1.5, 1.5, -0.7, "Re(c)") | |
| self.imag_slider = SmoothSlider(370 + slider_width + 10, HEIGHT - 35, slider_width, 10, -1.5, 1.5, 0.27015, "Im(c)") | |
| def update_ui(self): | |
| """Обновление UI при изменении размеров окна""" | |
| self._init_ui() | |
| def _render_fractal(self): | |
| if self.fractal_type == 'mandelbrot': | |
| self.current_surface, self.actual_viewport = self.renderer.render_mandelbrot( | |
| self.viewport['x_min'], self.viewport['x_max'], | |
| self.viewport['y_min'], self.viewport['y_max'], | |
| self.viewport['max_iter'] | |
| ) | |
| else: | |
| c = self.viewport['c'] | |
| self.current_surface, self.actual_viewport = self.renderer.render_julia( | |
| self.viewport['x_min'], self.viewport['x_max'], | |
| self.viewport['y_min'], self.viewport['y_max'], | |
| self.viewport['max_iter'], c.real, c.imag | |
| ) | |
| self.needs_render = False | |
| def handle_event(self, event): | |
| if self.renderer.is_rendering and event.type not in [pygame.QUIT, pygame.KEYDOWN]: | |
| if event.type in [pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.KEYDOWN]: | |
| self.renderer.stop_rendering() | |
| return None | |
| mouse_pos = pygame.mouse.get_pos() | |
| self.back_btn.check_hover(mouse_pos) | |
| self.reset_btn.check_hover(mouse_pos) | |
| input_result = self.iter_input.handle_event(event) | |
| if input_result is not None: | |
| self.viewport['max_iter'] = input_result | |
| self.needs_render = True | |
| slider_changed = False | |
| if self.fractal_type == 'julia': | |
| if self.real_slider.handle_event(event): | |
| self.viewport['c'] = complex(self.real_slider.value, self.viewport['c'].imag) | |
| slider_changed = True | |
| if self.imag_slider.handle_event(event): | |
| self.viewport['c'] = complex(self.viewport['c'].real, self.imag_slider.value) | |
| slider_changed = True | |
| if slider_changed: | |
| self.needs_render = True | |
| if event.type == pygame.QUIT: | |
| return 'quit' | |
| elif event.type == pygame.KEYDOWN: | |
| if event.key == pygame.K_ESCAPE: | |
| return 'menu' | |
| elif event.key == pygame.K_r: | |
| self._reset_view() | |
| self.needs_render = True | |
| elif event.key in [pygame.K_PLUS, pygame.K_EQUALS, pygame.K_KP_PLUS]: | |
| self.viewport['max_iter'] = min(self.viewport['max_iter'] + 20, 10000) | |
| self.iter_input.set_value(self.viewport['max_iter']) | |
| self.needs_render = True | |
| elif event.key in [pygame.K_MINUS, pygame.K_KP_MINUS]: | |
| self.viewport['max_iter'] = max(self.viewport['max_iter'] - 20, 10) | |
| self.iter_input.set_value(self.viewport['max_iter']) | |
| self.needs_render = True | |
| elif event.key == pygame.K_c and self.fractal_type == 'julia': | |
| new_c = complex(random.uniform(-1, 1), random.uniform(-1, 1)) | |
| self.viewport['c'] = new_c | |
| if self.real_slider and self.imag_slider: | |
| self.real_slider.value = new_c.real | |
| self.imag_slider.value = new_c.imag | |
| self.needs_render = True | |
| elif event.key == pygame.K_F11: | |
| self.app.toggle_fullscreen() | |
| elif event.type == pygame.MOUSEBUTTONDOWN: | |
| if event.button == 1: | |
| if self.back_btn.is_clicked(mouse_pos, event): | |
| return 'menu' | |
| elif self.reset_btn.is_clicked(mouse_pos, event): | |
| self._reset_view() | |
| self.needs_render = True | |
| elif mouse_pos[1] < FRACTAL_HEIGHT: | |
| self.nav.start_pan(mouse_pos) | |
| self.preview_offset = (0, 0) | |
| elif event.button == 4: | |
| self._zoom(1.5) | |
| self.needs_render = True | |
| elif event.button == 5: | |
| self._zoom(0.75) | |
| self.needs_render = True | |
| elif event.type == pygame.MOUSEBUTTONUP: | |
| if event.button == 1 and self.nav.mode == "panning": | |
| delta = self.nav.end_pan() | |
| if delta[0] != 0 or delta[1] != 0: | |
| self._apply_pan(delta) | |
| self.needs_render = True | |
| self.preview_offset = (0, 0) | |
| elif event.type == pygame.MOUSEMOTION: | |
| if self.nav.mode == "panning": | |
| self.nav.update_pan(mouse_pos) | |
| if self.nav.start_pos: | |
| dx = mouse_pos[0] - self.nav.start_pos[0] | |
| dy = mouse_pos[1] - self.nav.start_pos[1] | |
| self.preview_offset = (dx, dy) | |
| return None | |
| def _apply_pan(self, delta): | |
| """Применяем смещение с учетом текущих пропорций viewport""" | |
| dx, dy = delta | |
| if self.actual_viewport: | |
| x_min, x_max, y_min, y_max = self.actual_viewport | |
| else: | |
| x_min, x_max = self.viewport['x_min'], self.viewport['x_max'] | |
| y_min, y_max = self.viewport['y_min'], self.viewport['y_max'] | |
| width = x_max - x_min | |
| height = y_max - y_min | |
| dx_world = dx * width / WIDTH | |
| dy_world = dy * height / FRACTAL_HEIGHT | |
| self.viewport['x_min'] -= dx_world | |
| self.viewport['x_max'] -= dx_world | |
| self.viewport['y_min'] -= dy_world | |
| self.viewport['y_max'] -= dy_world | |
| def _zoom(self, factor): | |
| """Масштабирование с центром в середине экрана""" | |
| if self.actual_viewport: | |
| x_min, x_max, y_min, y_max = self.actual_viewport | |
| else: | |
| x_min, x_max = self.viewport['x_min'], self.viewport['x_max'] | |
| y_min, y_max = self.viewport['y_min'], self.viewport['y_max'] | |
| width = x_max - x_min | |
| height = y_max - y_min | |
| center_x = (x_min + x_max) / 2 | |
| center_y = (y_min + y_max) / 2 | |
| new_width = width / factor | |
| new_height = height / factor | |
| self.viewport['x_min'] = center_x - new_width / 2 | |
| self.viewport['x_max'] = center_x + new_width / 2 | |
| self.viewport['y_min'] = center_y - new_height / 2 | |
| self.viewport['y_max'] = center_y + new_height / 2 | |
| def _reset_view(self): | |
| self.viewport['max_iter'] = 100 | |
| self.iter_input.set_value(100) | |
| if self.fractal_type == 'mandelbrot': | |
| self.viewport.update({'x_min': -2.5, 'x_max': 1.0, | |
| 'y_min': -1.0, 'y_max': 1.0}) | |
| else: | |
| self.viewport.update({'x_min': -2.0, 'x_max': 2.0, | |
| 'y_min': -1.5, 'y_max': 1.5}) | |
| self.viewport['c'] = complex(-0.7, 0.27015) | |
| if self.real_slider and self.imag_slider: | |
| self.real_slider.value = -0.7 | |
| self.imag_slider.value = 0.27015 | |
| def update(self): | |
| self.fps_counter.update() | |
| if self.needs_render and not self.renderer.is_rendering: | |
| self._render_fractal() | |
| def draw(self, screen): | |
| # Фон фрактала | |
| screen.fill((0, 0, 0), (0, 0, WIDTH, FRACTAL_HEIGHT)) | |
| # Отрисовка фрактала с панорамированием | |
| if self.current_surface: | |
| if self.nav.mode == "panning" and self.preview_offset != (0, 0): | |
| dx, dy = self.preview_offset | |
| src_rect = pygame.Rect(0, 0, WIDTH, FRACTAL_HEIGHT) | |
| if dx > 0: | |
| src_rect.width -= dx | |
| else: | |
| src_rect.x = -dx | |
| src_rect.width += dx | |
| if dy > 0: | |
| src_rect.height -= dy | |
| else: | |
| src_rect.y = -dy | |
| src_rect.height += dy | |
| if src_rect.width > 0 and src_rect.height > 0: | |
| visible_part = self.current_surface.subsurface(src_rect) | |
| dst_x = max(0, dx) | |
| dst_y = max(0, dy) | |
| screen.blit(visible_part, (dst_x, dst_y)) | |
| else: | |
| screen.blit(self.current_surface, (0, 0)) | |
| # Панель управления | |
| pygame.draw.rect(screen, (50, 50, 50), (0, FRACTAL_HEIGHT, WIDTH, MENU_HEIGHT)) | |
| pygame.draw.line(screen, (70, 70, 70), (0, FRACTAL_HEIGHT), | |
| (WIDTH, FRACTAL_HEIGHT), 2) | |
| # Кнопки на панели | |
| self.back_btn.draw(screen) | |
| self.reset_btn.draw(screen) | |
| # Поле ввода итераций | |
| screen.blit(self.iter_label, self.iter_label_pos) | |
| self.iter_input.draw(screen) | |
| # Ползунки для Жюлиа | |
| if self.fractal_type == 'julia': | |
| if self.real_slider: | |
| self.real_slider.draw(screen) | |
| if self.imag_slider: | |
| self.imag_slider.draw(screen) | |
| # ===== ИНФОРМАЦИЯ ПОВЕРХ ФРАКТАЛА ===== | |
| # 1. Информация о фрактале (левый верхний угол) | |
| info_font = pygame.font.SysFont('Arial', 16, bold=True) | |
| if self.fractal_type == 'mandelbrot': | |
| info = "МАНДЕЛЬБРОТ" | |
| else: | |
| c = self.viewport['c'] | |
| info = f"ЖЮЛИА | c = {c.real:.3f} + {c.imag:.3f}i" | |
| info_text = info_font.render(info, True, (255, 255, 255)) | |
| info_bg = pygame.Surface((info_text.get_width() + 10, info_text.get_height() + 6), pygame.SRCALPHA) | |
| info_bg.fill((0, 0, 0, 180)) # Полупрозрачный чёрный | |
| screen.blit(info_bg, (10, 10)) | |
| screen.blit(info_text, (15, 13)) | |
| # 2. FPS (правый верхний угол) | |
| fps_font = pygame.font.SysFont('Arial', 16, bold=True) | |
| fps_color = (100, 255, 100) if self.fps_counter.fps > 30 else \ | |
| (255, 200, 100) if self.fps_counter.fps > 15 else \ | |
| (255, 100, 100) | |
| fps_text = fps_font.render(f"FPS: {self.fps_counter.fps}", True, fps_color) | |
| fps_bg = pygame.Surface((fps_text.get_width() + 10, fps_text.get_height() + 6), pygame.SRCALPHA) | |
| fps_bg.fill((0, 0, 0, 180)) | |
| screen.blit(fps_bg, (WIDTH - fps_text.get_width() - 15, 10)) | |
| screen.blit(fps_text, (WIDTH - fps_text.get_width() - 10, 13)) | |
| # === Главное приложение === | |
| class FractalApp: | |
| def __init__(self): | |
| self.fullscreen = False | |
| self.screen = pygame.display.set_mode((INIT_WIDTH, INIT_HEIGHT)) | |
| pygame.display.set_caption("Визуализатор Фракталов ULTRA") | |
| self.clock = pygame.time.Clock() | |
| self.state = "menu" | |
| self.current_viewer = None | |
| self.menu = MainMenu(self) | |
| self.running = True | |
| def toggle_fullscreen(self): | |
| """Переключение полноэкранного режима""" | |
| self.fullscreen = not self.fullscreen | |
| if self.fullscreen: | |
| # Полноэкранный режим | |
| self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN) | |
| else: | |
| # Оконный режим | |
| self.screen = pygame.display.set_mode((INIT_WIDTH, INIT_HEIGHT)) | |
| # Обновляем глобальные переменные размеров | |
| global WIDTH, HEIGHT, FRACTAL_HEIGHT | |
| WIDTH, HEIGHT = self.screen.get_size() | |
| FRACTAL_HEIGHT = HEIGHT - MENU_HEIGHT | |
| # Обновляем UI | |
| if self.state == "menu": | |
| self.menu.update_ui() | |
| elif self.state == "fractal" and self.current_viewer: | |
| self.current_viewer.update_ui() | |
| self.current_viewer.needs_render = True | |
| def run(self): | |
| while self.running: | |
| for event in pygame.event.get(): | |
| if event.type == pygame.QUIT: | |
| self._safe_shutdown() | |
| return | |
| if self.state == "menu": | |
| result = self.menu.handle_event(event) | |
| if result == 'mandelbrot': | |
| self._transition_to_fractal('mandelbrot') | |
| elif result == 'julia': | |
| self._transition_to_fractal('julia') | |
| elif result == 'quit': | |
| self._safe_shutdown() | |
| return | |
| elif self.state == "fractal" and self.current_viewer: | |
| result = self.current_viewer.handle_event(event) | |
| if result == 'menu': | |
| self._transition_to_menu() | |
| elif result == 'quit': | |
| self._safe_shutdown() | |
| return | |
| if self.state == "fractal" and self.current_viewer: | |
| self.current_viewer.update() | |
| if self.state == "menu": | |
| self.menu.draw(self.screen) | |
| elif self.state == "fractal" and self.current_viewer: | |
| self.current_viewer.draw(self.screen) | |
| pygame.display.flip() | |
| self.clock.tick(60) | |
| pygame.quit() | |
| sys.exit() | |
| def _transition_to_fractal(self, fractal_type): | |
| if self.current_viewer and self.current_viewer.renderer.is_rendering: | |
| self.current_viewer.renderer.stop_rendering() | |
| for _ in range(10): | |
| if not self.current_viewer.renderer.is_rendering: | |
| break | |
| pygame.time.wait(10) | |
| self.current_viewer = FractalViewer(self, fractal_type) | |
| self.state = "fractal" | |
| def _transition_to_menu(self): | |
| if self.current_viewer and self.current_viewer.renderer.is_rendering: | |
| self.current_viewer.renderer.stop_rendering() | |
| for _ in range(10): | |
| if not self.current_viewer.renderer.is_rendering: | |
| break | |
| pygame.time.wait(10) | |
| self.current_viewer = None | |
| self.state = "menu" | |
| def _safe_shutdown(self): | |
| self.running = False | |
| # === Запуск === | |
| if __name__ == "__main__": | |
| print("=" * 50) | |
| print("ВИЗУАЛИЗАТОР ФРАКТАЛОВ - ФИНАЛЬНАЯ ВЕРСИЯ С ИСПРАВЛЕННЫМИ ПРОПОРЦИЯМИ") | |
| print("=" * 50) | |
| try: | |
| from numba import njit | |
| print("✓ Numba обнаружена - ультра-ускоренный режим") | |
| except ImportError: | |
| print("✗ Внимание: Numba не установлена!") | |
| print(" Установите: pip install numba") | |
| sys.exit(1) | |
| app = FractalApp() | |
| app.run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment