Skip to content

Instantly share code, notes, and snippets.

@Dan21097
Created December 12, 2025 22:01
Show Gist options
  • Select an option

  • Save Dan21097/d548394c5a6dd000247f54c42f858579 to your computer and use it in GitHub Desktop.

Select an option

Save Dan21097/d548394c5a6dd000247f54c42f858579 to your computer and use it in GitHub Desktop.
for gpt
"""ВИЗУАЛИЗАТОР ФРАКТАЛОВ - ИСПРАВЛЕННЫЙ ПОЛНОЭКРАННЫЙ РЕЖИМ И СОХРАНЕНИЕ ПРОПОРЦИЙ"""
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