Практика сделана

This commit is contained in:
Данила Копылов 2025-05-18 18:12:16 +03:00
commit d225612d2e
15 changed files with 789 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

49
climate_zones_artistic.py Normal file
View File

@ -0,0 +1,49 @@
import numpy as np
import random
from settings import WorldSettings
class ArtisticClimateGenerator:
def __init__(self, height_map: np.ndarray, settings: WorldSettings):
self.height_map = height_map
self.settings = settings
random.seed(settings.get_valid_seed())
# Оригинальные цвета из ClimateGenerator
self.biome_colors = {
'water': (0.2, 0.4, 0.8, 1.0),
'ice': (0.95, 0.95, 0.98, 0.8),
'tundra': (0.4, 0.6, 0.9, 0.9),
'taiga': (0.1, 0.3, 0.15, 1.0),
'forest': (0.3, 0.6, 0.3, 1.0),
'desert': (0.9, 0.6, 0.2, 1.0),
'steppe': (0.8, 0.75, 0.5, 1.0),
'swamp': (0.4, 0.35, 0.2, 0.9),
'jungle': (0.1, 0.5, 0.1, 1.0),
'savanna': (0.8, 0.7, 0.3, 1.0)
}
def generate_biome_map(self, enabled_biomes=None) -> np.ndarray:
"""Полностью случайное распределение биомов"""
if enabled_biomes is None:
enabled_biomes = list(self.biome_colors.keys())
height, width = self.height_map.shape
biome_map = np.zeros((height, width, 4), dtype=np.float32)
# Включаем воду, если она есть в списке
water_included = 'water' in enabled_biomes
water_level = 0.05
for y in range(height):
for x in range(width):
# Если это вода - рисуем воду и переходим к следующей точке
if water_included and self.height_map[y,x] < water_level:
biome_map[y,x] = self.biome_colors['water']
continue
# Случайный выбор любого биома
available_biomes = [b for b in enabled_biomes if b != 'water'] or ['forest']
random_biome = random.choice(available_biomes)
biome_map[y,x] = self.biome_colors[random_biome]
return biome_map

117
climate_zones_map.py Normal file
View File

@ -0,0 +1,117 @@
import numpy as np
from opensimplex import OpenSimplex
from settings import WorldSettings
from numba import njit
class ClimateGenerator:
def __init__(self, height_map: np.ndarray, settings: WorldSettings):
self.height_map = height_map
self.settings = settings
self.noise = OpenSimplex(seed=settings.get_valid_seed())
# Палитра биомов
self.biome_colors = {
'water': (0.2, 0.4, 0.8, 1.0),
'ice': (0.95, 0.95, 0.98, 0.8),
'tundra': (0.4, 0.6, 0.9, 0.9),
'taiga': (0.1, 0.3, 0.15, 1.0),
'forest': (0.3, 0.6, 0.3, 1.0),
'desert': (0.9, 0.6, 0.2, 1.0),
'steppe': (0.8, 0.75, 0.5, 1.0),
'swamp': (0.4, 0.35, 0.2, 0.9),
'jungle': (0.1, 0.5, 0.1, 1.0),
'savanna': (0.8, 0.7, 0.3, 1.0)
}
def generate_biome_map(self, enabled_biomes=None) -> np.ndarray:
"""Генерация карты биомов с учетом выбранных биомов"""
if enabled_biomes is None:
enabled_biomes = list(self.biome_colors.keys())
height, width = self.height_map.shape
# Генерация шумов
temp_noise = self._generate_noise(width, height, 40)
moist_noise = self._generate_noise(width, height, 50)
biome_map = np.zeros((height, width, 4), dtype=np.float32)
self._fill_biome_map(
biome_map,
self.height_map,
temp_noise,
moist_noise,
enabled_biomes
)
return biome_map
def _generate_noise(self, width, height, scale):
"""Генерация многооктавного шума"""
noise = np.zeros((height, width))
for octave in [1.0, 0.5, 0.25]:
for y in range(height):
for x in range(width):
nx = x / (scale * octave)
ny = y / (scale * octave)
noise[y, x] += self.noise.noise2(nx, ny) * octave
return (noise - noise.min()) / (noise.max() - noise.min())
@staticmethod
@njit
def _fill_biome_map(biome_map, height_map, temp_noise, moist_noise, enabled_biomes):
height, width = height_map.shape
water_level = 0.05
for y in range(height):
latitude = abs(y/height - 0.5) * 2 # 0 на экваторе, 1 на полюсах
for x in range(width):
h = height_map[y,x]
# Вода (всегда включена, без прозрачности)
if h < water_level:
biome_map[y,x] = (0.2, 0.4, 0.8, 1.0)
continue
# Климатические параметры
temp = (0.7 - latitude * 0.5) * (1.0 - h * 0.5) * (0.8 + temp_noise[y,x] * 0.4)
moist = moist_noise[y,x]
# Расчёт прозрачности: сильнее зависит от высоты
alpha = 1.0 - h**2 * 0.7 # h^2 делает прозрачность резче на вершинах
# Выбор биома
if temp < 0.3: # Холодные зоны
if h > 0.8 and 'ice' in enabled_biomes:
biome = (0.95, 0.95, 0.98, alpha * 0.7) # Лёд прозрачнее
elif moist > 0.65 and 'tundra' in enabled_biomes:
biome = (0.4, 0.6, 0.9, alpha)
elif 'taiga' in enabled_biomes:
biome = (0.1, 0.3, 0.15, alpha)
else:
biome = (0.3, 0.6, 0.3, alpha)
elif temp < 0.6: # Умеренные зоны
if moist > 0.7 and 'swamp' in enabled_biomes:
biome = (0.4, 0.35, 0.2, alpha)
elif moist > 0.5 and 'forest' in enabled_biomes:
biome = (0.3, 0.6, 0.3, alpha)
elif moist > 0.3 and 'steppe' in enabled_biomes:
biome = (0.8, 0.75, 0.5, alpha)
elif 'desert' in enabled_biomes:
biome = (0.9, 0.6, 0.2, alpha)
else:
biome = (0.3, 0.6, 0.3, alpha)
else: # Теплые зоны
if moist < 0.3 and 'desert' in enabled_biomes:
biome = (0.9, 0.6, 0.2, alpha)
elif moist < 0.5 and 'savanna' in enabled_biomes:
biome = (0.8, 0.7, 0.3, alpha)
elif moist > 0.7 and 'jungle' in enabled_biomes:
biome = (0.1, 0.5, 0.1, alpha)
elif 'forest' in enabled_biomes:
biome = (0.3, 0.6, 0.3, alpha)
else:
biome = (0.8, 0.75, 0.5, alpha)
biome_map[y,x] = biome

391
frontend.py Normal file
View File

@ -0,0 +1,391 @@
import tkinter as tk
from tkinter import ttk
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from generation_landwater import WorldGenerator
from generation_heightmap import HeightmapGenerator
from climate_zones_map import ClimateGenerator
from settings import WorldSettings
from map_styler import MapStyler
import threading
import tempfile
import os
from PIL import Image, ImageTk
class WorldGeneratorApp:
def __init__(self, root):
self.root = root
self.settings = WorldSettings()
self._init_variables()
self._setup_ui()
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _init_variables(self):
"""Инициализация переменных"""
self.width_var = tk.IntVar(value=self.settings.width)
self.height_var = tk.IntVar(value=self.settings.height)
self.mode_var = tk.StringVar(value=self.settings.mode)
# Параметры рельефа
self.terrain_rough_var = tk.DoubleVar(value=self.settings.terrain_roughness)
self.continent_size_var = tk.DoubleVar(value=self.settings.continent_size)
self.continent_rough_var = tk.DoubleVar(value=self.settings.continent_roughness)
self.islands_dens_var = tk.DoubleVar(value=self.settings.islands_density)
self.islands_rough_var = tk.DoubleVar(value=self.settings.islands_roughness)
self.island_size_var = tk.DoubleVar(value=self.settings.island_size)
# Биомы
self.biome_vars = {
'ice': tk.BooleanVar(value=True),
'tundra': tk.BooleanVar(value=True),
'taiga': tk.BooleanVar(value=True),
'forest': tk.BooleanVar(value=True),
'desert': tk.BooleanVar(value=True),
'steppe': tk.BooleanVar(value=True),
'swamp': tk.BooleanVar(value=True),
'jungle': tk.BooleanVar(value=True),
'savanna': tk.BooleanVar(value=True)
}
# Данные карт
self.height_map = None
self.biome_map = None
self.map_window = None
self.canvas = None
def _setup_ui(self):
"""Настройка интерфейса"""
self.root.title("Генератор мира")
self.root.geometry("900x650")
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Панель управления
control_frame = ttk.LabelFrame(main_frame, text="Управление")
control_frame.pack(fill=tk.X, pady=5)
# Первый ряд кнопок
btn_row1 = ttk.Frame(control_frame)
btn_row1.pack(fill=tk.X)
buttons_row1 = [
("Сгенерировать землю", self.generate_land),
("Сгенерировать высоты", self.generate_height),
("Сгенерировать климат", self.generate_climate),
("Показать карту", self.show_map)
]
for i, (text, cmd) in enumerate(buttons_row1):
ttk.Button(btn_row1, text=text, command=cmd).grid(
row=0, column=i, padx=5, pady=5, sticky="ew")
btn_row1.grid_columnconfigure(i, weight=1)
# Второй ряд кнопок
btn_row2 = ttk.Frame(control_frame)
btn_row2.pack(fill=tk.X, pady=5)
buttons_row2 = [
("АВТО", self.auto_generate),
("Стилизовать под свиток", self.style_map),
("Настройки", self.open_settings)
]
for i, (text, cmd) in enumerate(buttons_row2):
ttk.Button(btn_row2, text=text, command=cmd).grid(
row=0, column=i, padx=5, pady=5, sticky="ew")
btn_row2.grid_columnconfigure(i, weight=1)
# Статус бар
self.status_var = tk.StringVar(value="Готов к работе")
ttk.Label(main_frame, textvariable=self.status_var,
relief=tk.SUNKEN, anchor=tk.W).pack(fill=tk.X, pady=5)
def _on_close(self):
"""Корректное закрытие приложения"""
if self.map_window:
self.map_window.destroy()
plt.close('all')
self.root.destroy()
def auto_generate(self):
"""Автоматическая генерация без стилизации"""
def _generate():
try:
self.status_var.set("Автогенерация: создание земли...")
self.generate_land()
self.status_var.set("Автогенерация: создание высот...")
self.generate_height()
self.status_var.set("Автогенерация: создание климата...")
self.generate_climate()
self.status_var.set("Автогенерация завершена!")
self.show_map()
except Exception as e:
self.status_var.set(f"Ошибка: {str(e)}")
threading.Thread(target=_generate, daemon=True).start()
def open_settings(self):
settings_win = tk.Toplevel(self.root)
settings_win.title("Настройки генерации")
settings_win.geometry("600x500")
notebook = ttk.Notebook(settings_win)
# Основные настройки
basic_frame = ttk.Frame(notebook)
self.create_basic_settings(basic_frame)
notebook.add(basic_frame, text="Основные")
# Настройки биомов
biome_frame = ttk.Frame(notebook)
self.create_biome_settings(biome_frame)
notebook.add(biome_frame, text="Биомы")
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# Кнопки управления
btn_frame = ttk.Frame(settings_win)
ttk.Button(btn_frame, text="Применить",
command=lambda: [self.save_settings(), settings_win.destroy()]).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Отмена",
command=settings_win.destroy).pack(side=tk.LEFT, padx=5)
btn_frame.pack(pady=10)
def style_map(self):
"""Стилизация текущей карты под старинный свиток"""
if not hasattr(self, 'biome_map') and not hasattr(self, 'height_map'):
self.status_var.set("Ошибка: нет данных для стилизации")
return
try:
self.status_var.set("Стилизация...")
self.root.update_idletasks()
# Подготовка данных
if hasattr(self, 'biome_map') and self.biome_map is not None:
img_data = (self.biome_map[:, :, :3] * 255).astype(np.uint8)
else:
norm_height = (self.height_map - self.height_map.min()) / (self.height_map.max() - self.height_map.min())
img_data = (plt.cm.terrain(norm_height)[:, :, :3] * 255).astype(np.uint8)
# Создаем PIL Image и применяем стилизацию
pil_img = Image.fromarray(img_data, 'RGB')
styled_img = MapStyler.apply_parchment_effect(pil_img)
# Отображаем результат
self.preview_window = tk.Toplevel(self.root)
self.preview_window.title("Стилизованная карта")
img_tk = ImageTk.PhotoImage(styled_img)
label = ttk.Label(self.preview_window, image=img_tk)
label.image = img_tk
label.pack()
self.status_var.set("Стилизация завершена!")
except Exception as e:
self.status_var.set(f"Ошибка стилизации: {str(e)}")
def create_basic_settings(self, frame):
"""Настройки размера и типа карты"""
# Размер карты
size_frame = ttk.LabelFrame(frame, text="Размер карты")
size_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(size_frame, text="Ширина:").grid(row=0, column=0, sticky="e")
ttk.Spinbox(size_frame, from_=100, to=1000, textvariable=self.width_var).grid(
row=0, column=1, sticky="ew", padx=5)
ttk.Label(size_frame, text="Высота:").grid(row=1, column=0, sticky="e")
ttk.Spinbox(size_frame, from_=100, to=1000, textvariable=self.height_var).grid(
row=1, column=1, sticky="ew", padx=5)
# Тип генерации
type_frame = ttk.LabelFrame(frame, text="Тип ландшафта")
type_frame.pack(fill=tk.X, padx=5, pady=5)
types = ["land_only", "continent", "islands"]
texts = ["Только суша", "Материк", "Острова"]
for t, text in zip(types, texts):
ttk.Radiobutton(type_frame, text=text, variable=self.mode_var, value=t).pack(
anchor=tk.W)
# Параметры рельефа
terrain_frame = ttk.LabelFrame(frame, text="Параметры рельефа")
terrain_frame.pack(fill=tk.X, padx=5, pady=5)
params = [
("Шероховатость:", self.terrain_rough_var),
("Размер материка:", self.continent_size_var),
("Шероховатость материка:", self.continent_rough_var),
("Плотность островов:", self.islands_dens_var),
("Шероховатость островов:", self.islands_rough_var),
("Размер островов:", self.island_size_var)
]
for i, (text, var) in enumerate(params):
ttk.Label(terrain_frame, text=text).grid(row=i, column=0, sticky="e", padx=5)
ttk.Scale(terrain_frame, from_=0.1, to=1.0, variable=var,
orient=tk.HORIZONTAL).grid(row=i, column=1, sticky="ew", padx=5)
ttk.Label(terrain_frame, textvariable=var).grid(row=i, column=2, padx=5)
# Режим климата
climate_frame = ttk.LabelFrame(frame, text="Режим климата")
climate_frame.pack(fill=tk.X, padx=5, pady=5)
if not hasattr(self.settings, 'climate_mode'):
self.settings.climate_mode = 'realistic'
ttk.Radiobutton(climate_frame, text="Реалистичный",
variable=self.settings.climate_mode,
value="realistic").pack(anchor=tk.W)
ttk.Radiobutton(climate_frame, text="Художественный",
variable=self.settings.climate_mode,
value="artistic").pack(anchor=tk.W)
def create_biome_settings(self, frame):
"""Настройки биомов через чекбоксы"""
biome_frame = ttk.LabelFrame(frame, text="Выбор биомов")
biome_frame.pack(fill=tk.BOTH, padx=5, pady=5, expand=True)
for i, (biome, var) in enumerate(self.biome_vars.items()):
col = i % 3
row = i // 3
ttk.Checkbutton(
biome_frame,
text=biome.capitalize(),
variable=var
).grid(row=row, column=col, sticky="w", padx=5, pady=2)
def save_settings(self):
"""Сохраняет все настройки"""
try:
self.settings.width = self.width_var.get()
self.settings.height = self.height_var.get()
self.settings.mode = self.mode_var.get()
self.settings.terrain_roughness = self.terrain_rough_var.get()
self.settings.continent_size = self.continent_size_var.get()
self.settings.continent_roughness = self.continent_rough_var.get()
self.settings.islands_density = self.islands_dens_var.get()
self.settings.islands_roughness = self.islands_rough_var.get()
self.settings.island_size = self.island_size_var.get()
self.status_var.set("Настройки сохранены")
except Exception as e:
self.status_var.set(f"Ошибка сохранения: {str(e)}")
def generate_land(self):
"""Генерация карты земли/воды"""
try:
self.land_generator = WorldGenerator(
width=self.settings.width,
height=self.settings.height,
seed=self.settings.get_valid_seed()
)
if self.settings.mode == 'land_only':
self.height_map = self.land_generator.generate_land_only()
elif self.settings.mode == 'continent':
self.height_map = self.land_generator.generate_continent(
size=self.settings.continent_size,
roughness=self.settings.continent_roughness
)
else:
self.height_map = self.land_generator.generate_islands(
density=self.settings.islands_density,
island_size=self.settings.island_size,
roughness=self.settings.islands_roughness
)
self.biome_map = None
self.styled_map_path = None
self.status_var.set("Карта земли сгенерирована")
except Exception as e:
self.status_var.set(f"Ошибка генерации земли: {str(e)}")
def generate_height(self):
"""Генерация карты высот"""
if not hasattr(self, 'height_map') or self.height_map is None:
self.status_var.set("Ошибка: сначала сгенерируйте карту земли")
return
try:
self.height_generator = HeightmapGenerator(
width=self.settings.width,
height=self.settings.height,
seed=self.settings.get_valid_seed()
)
self.height_map = self.height_generator.generate_heightmap(
self.height_map,
roughness=self.settings.terrain_roughness
)
self.biome_map = None
self.styled_map_path = None
self.status_var.set("Карта высот сгенерирована")
except Exception as e:
self.status_var.set(f"Ошибка генерации высот: {str(e)}")
def generate_climate(self):
if not hasattr(self, 'height_map'):
self.status_var.set("Ошибка: сначала сгенерируйте карту высот")
return
try:
enabled_biomes = [b for b, var in self.biome_vars.items() if var.get()]
if self.settings.climate_mode == 'realistic':
from climate_zones_map import ClimateGenerator
self.climate_gen = ClimateGenerator(self.height_map, self.settings)
else:
from climate_zones_artistic import ArtisticClimateGenerator
self.climate_gen = ArtisticClimateGenerator(self.height_map, self.settings)
self.biome_map = self.climate_gen.generate_biome_map(['water'] + enabled_biomes)
self.status_var.set(f"Карта климата ({self.settings.climate_mode}) сгенерирована")
except Exception as e:
self.status_var.set(f"Ошибка генерации климата: {str(e)}")
def show_map(self):
"""Отображение карты во встроенном окне"""
if not hasattr(self, 'biome_map') and not hasattr(self, 'height_map'):
self.status_var.set("Нет данных для отображения")
return
if self.map_window:
self.map_window.destroy()
self.map_window = tk.Toplevel(self.root)
self.map_window.title("Просмотр карты")
fig = plt.Figure(figsize=(8, 6))
ax = fig.add_subplot(111)
if hasattr(self, 'biome_map') and self.biome_map is not None:
ax.imshow(self.biome_map)
ax.set_title("Карта биомов")
else:
ax.imshow(self.height_map, cmap='terrain')
ax.set_title("Карта высот")
ax.axis('off')
self.canvas = FigureCanvasTkAgg(fig, master=self.map_window)
self.canvas.draw()
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# Кнопка закрытия
ttk.Button(self.map_window, text="Закрыть",
command=self.map_window.destroy).pack(pady=5)
if __name__ == "__main__":
root = tk.Tk()
app = WorldGeneratorApp(root)
root.mainloop()

40
generation_heightmap.py Normal file
View File

@ -0,0 +1,40 @@
import numpy as np
from opensimplex import OpenSimplex
class HeightmapGenerator:
def __init__(self, width=200, height=200, seed=None):
self.width = width
self.height = height
self.seed = seed if seed is not None else np.random.randint(0, 1000000)
self.noise = OpenSimplex(seed=self.seed)
def generate_heightmap(self, land_water_map, roughness=0.5):
heightmap = np.zeros((self.height, self.width))
scale = 30 / (roughness + 0.1)
base_noise = np.zeros((self.height, self.width))
for y in range(self.height):
for x in range(self.width):
nx = x / scale
ny = y / scale
base_noise[y,x] = self.noise.noise2(nx, ny)
base_noise = (base_noise - base_noise.min()) / (base_noise.max() - base_noise.min())
detail_scale = scale * 0.3
detail_noise = np.zeros((self.height, self.width))
for y in range(self.height):
for x in range(self.width):
nx = x / detail_scale
ny = y / detail_scale
detail_noise[y,x] = self.noise.noise2(nx, ny) * 0.3
combined = base_noise + detail_noise
combined = np.where(land_water_map == 1, combined, 0)
combined = (combined - combined.min()) / (combined.max() - combined.min())
heightmap = np.where(land_water_map == 1,
np.round(combined * 9 + 1) / 10,
0)
return heightmap

79
generation_landwater.py Normal file
View File

@ -0,0 +1,79 @@
import numpy as np
import opensimplex
import random
class WorldGenerator:
def __init__(self, width=200, height=200, seed=None):
self.width = width
self.height = height
self.seed = seed if seed is not None else random.randint(0, 1000000)
self.noise = opensimplex.OpenSimplex(seed=self.seed)
random.seed(self.seed)
def generate_land_only(self):
return np.ones((self.height, self.width))
def generate_continent(self, size=0.7, roughness=0.5):
world = np.zeros((self.height, self.width))
center_x, center_y = self.width//2, self.height//2
for y in range(self.height):
for x in range(self.width):
nx = (x - center_x) / (self.width * 0.8)
ny = (y - center_y) / (self.height * 0.8)
dist = np.sqrt(nx**2 + ny**2) * (1.0/size)
noise_val = self.noise.noise2(x * roughness/10, y * roughness/10)
world[y][x] = 1 if (1 - dist) + noise_val * 0.5 > 0.4 else 0
return world
def generate_islands(self, density=0.6, island_size=0.3, roughness=0.5):
world = np.zeros((self.height, self.width))
scale = 20 / (island_size + 0.1)
temp_map = np.zeros((self.height, self.width))
for y in range(self.height):
for x in range(self.width):
nx = x / scale * (1 + roughness)
ny = y / scale * (1 + roughness)
noise_val = self.noise.noise2(nx, ny)
threshold = 0.5 - density*0.4
if noise_val > threshold:
temp_map[y][x] = 1
islands = []
visited = np.zeros_like(temp_map)
for y in range(self.height):
for x in range(self.width):
if temp_map[y][x] == 1 and visited[y][x] == 0:
island = []
queue = [(y, x)]
visited[y][x] = 1
min_y, max_y = y, y
min_x, max_x = x, x
while queue:
cy, cx = queue.pop()
island.append((cy, cx))
min_y = min(min_y, cy)
max_y = max(max_y, cy)
min_x = min(min_x, cx)
max_x = max(max_x, cx)
for dy, dx in [(-1,0),(1,0),(0,-1),(0,1)]:
ny, nx = cy+dy, cx+dx
if (0 <= ny < self.height and 0 <= nx < self.width and
temp_map[ny][nx] == 1 and visited[ny][nx] == 0):
visited[ny][nx] = 1
queue.append((ny, nx))
if (min_y > 0 and max_y < self.height-1 and
min_x > 0 and max_x < self.width-1):
islands.append(island)
for island in islands:
for y, x in island:
world[y][x] = 1
return world

10
main.py Normal file
View File

@ -0,0 +1,10 @@
from frontend import WorldGeneratorApp
import tkinter as tk
def main():
root = tk.Tk()
app = WorldGeneratorApp(root)
root.mainloop()
if __name__ == "__main__":
main()

82
map_styler.py Normal file
View File

@ -0,0 +1,82 @@
import numpy as np
from PIL import Image, ImageOps, ImageFilter, ImageDraw, ImageEnhance
import random
class MapStyler:
@staticmethod
def apply_parchment_effect(pil_image, output_path=None):
"""
Применяет эффект старинного свитка к изображению PIL
Args:
pil_image: PIL Image object
output_path: необязательный путь для сохранения
Returns:
PIL Image с эффектом
"""
# Текстура пергамента
width, height = pil_image.size
parchment = MapStyler._create_parchment_texture(width, height)
# Эффект старения
result_img = Image.blend(parchment, pil_image, 0.6)
# Эффекты старения
result_img = result_img.filter(ImageFilter.SMOOTH_MORE)
result_img = ImageEnhance.Contrast(result_img).enhance(1.2)
result_img = ImageEnhance.Color(result_img).enhance(0.9)
# Декоративные элементы
result_img = MapStyler._add_decorations(result_img)
if output_path:
result_img.save(output_path, quality=95)
return result_img
@staticmethod
def _create_parchment_texture(width, height):
"""Создает текстуру пергамента"""
# Цвет пергамента
base = Image.new('RGB', (width, height), color=(240, 230, 200))
# Добавляем шум и неравномерность
noise = np.random.normal(0, 0.1, (height, width, 3)) * 255
noise = np.clip(noise, -30, 30)
noise_img = Image.fromarray(noise.astype(np.uint8), 'RGB')
return Image.blend(base, noise_img, 0.3)
@staticmethod
def _add_decorations(img):
"""Добавляет декоративные элементы"""
draw = ImageDraw.Draw(img)
width, height = img.size
# Рамка в стиле старинной книги
border_color = (139, 69, 19) # Коричневый
border_width = max(width, height) // 100
draw.rectangle([(0, 0), (width-1, height-1)], outline=border_color, width=border_width)
# Угловые узоры
corner_size = min(width, height) // 8
corners = [(0, 0, 180, 270),(width-corner_size, 0, 270, 360),(0, height-corner_size, 90, 180),(width-corner_size, height-corner_size, 0, 90)]
for x, y, start, end in corners:
draw.arc([x, y, x+corner_size, y+corner_size], start, end, fill=border_color, width=2)
# Эффект потертости по краям
mask = Image.new('L', (width, height), 255)
edge_width = min(width, height) // 15
for i in range(edge_width):
alpha = int(255 * (i / edge_width) ** 0.5)
for side in ['top', 'bottom', 'left', 'right']:
if side in ['top', 'bottom']:
box = [0, i if side == 'top' else height-1-i,
width, i+1 if side == 'top' else height-i]
else:
box = [i if side == 'left' else width-1-i, 0,
i+1 if side == 'left' else width-i, height]
mask_draw = ImageDraw.Draw(mask)
mask_draw.rectangle(box, fill=alpha)
return Image.composite(img, Image.new('RGB', img.size, (220, 210, 180)), mask)

21
settings.py Normal file
View File

@ -0,0 +1,21 @@
from dataclasses import dataclass
import numpy as np
@dataclass
class WorldSettings:
width: int = 400
height: int = 300
seed: int = None
mode: str = 'continent' # 'land_only', 'continent', 'islands'
# Параметры рельефа
terrain_roughness: float = 0.5
continent_size: float = 0.7
continent_roughness: float = 0.5
islands_density: float = 0.6
islands_roughness: float = 0.5
island_size: float = 0.5
climate_mode: str = 'realistic' # 'realistic' или 'artistic'
def get_valid_seed(self):
return self.seed if self.seed is not None else np.random.randint(0, 1000000)