StePe

Untitled

Mar 11th, 2026
35
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.56 KB | None | 0 0
  1. import math
  2. import random
  3. import sys
  4. import colorsys
  5. from dataclasses import dataclass
  6.  
  7. import pygame
  8.  
  9. GRID_SIZE = 32
  10. CELL_SIZE = 16
  11. GRID_PIXELS = GRID_SIZE * CELL_SIZE
  12. LEFT_GRAPH_WIDTH = 170
  13. BOTTOM_GRAPH_HEIGHT = 170
  14. RIGHT_UI_WIDTH = 310
  15. MARGIN = 20
  16. FPS = 60
  17. ROW_SHIFT_PIXELS = 1.8
  18. COL_SHIFT_PIXELS = 1.8
  19. DEFAULT_FADE_PER_FRAME = 0.99
  20. COLOR_SHIFT_DEFAULT = 0.10
  21. VARIATION_SPEED_DEFAULT = 1.00
  22. UI_SLIDER_START_Y = 80
  23. UI_SLIDER_GAP_Y = 62
  24.  
  25. WINDOW_WIDTH = MARGIN * 3 + LEFT_GRAPH_WIDTH + GRID_PIXELS + RIGHT_UI_WIDTH
  26. WINDOW_HEIGHT = MARGIN * 3 + GRID_PIXELS + BOTTOM_GRAPH_HEIGHT
  27.  
  28. BG = (15, 18, 24)
  29. GRID_BG = (24, 29, 40)
  30. GRID_LINE = (42, 49, 64)
  31. GRID_VALUE = (68, 210, 255)
  32. AXIS_COLOR = (230, 230, 240)
  33. GRAPH_X_COLOR = (255, 176, 60)
  34. GRAPH_Y_COLOR = (92, 245, 174)
  35. TEXT = (230, 230, 240)
  36. TRACK = (50, 56, 70)
  37. FILL = (70, 130, 255)
  38. KNOB = (235, 239, 248)
  39. PANEL = (22, 26, 36)
  40. GRAPH_GRID = (44, 50, 64)
  41.  
  42.  
  43. def clamp(value: float, lo: float, hi: float) -> float:
  44. return max(lo, min(hi, value))
  45.  
  46.  
  47. class Perlin1D:
  48. def __init__(self, seed: int):
  49. rng = random.Random(seed)
  50. p = list(range(256))
  51. rng.shuffle(p)
  52. self.perm = p + p
  53.  
  54. @staticmethod
  55. def _fade(t: float) -> float:
  56. return t * t * t * (t * (t * 6 - 15) + 10)
  57.  
  58. @staticmethod
  59. def _lerp(a: float, b: float, t: float) -> float:
  60. return a + t * (b - a)
  61.  
  62. def _grad(self, h: int, x: float) -> float:
  63. return x if (h & 1) == 0 else -x
  64.  
  65. def noise(self, x: float) -> float:
  66. xi = math.floor(x) & 255
  67. xf = x - math.floor(x)
  68. u = self._fade(xf)
  69. a = self.perm[xi]
  70. b = self.perm[xi + 1]
  71. return self._lerp(self._grad(a, xf), self._grad(b, xf - 1.0), u)
  72.  
  73.  
  74. class Perlin2D:
  75. def __init__(self, seed: int):
  76. rng = random.Random(seed)
  77. p = list(range(256))
  78. rng.shuffle(p)
  79. self.perm = p + p
  80.  
  81. @staticmethod
  82. def _fade(t: float) -> float:
  83. return t * t * t * (t * (t * 6 - 15) + 10)
  84.  
  85. @staticmethod
  86. def _lerp(a: float, b: float, t: float) -> float:
  87. return a + t * (b - a)
  88.  
  89. def _grad(self, h: int, x: float, y: float) -> float:
  90. h = h & 7
  91. if h == 0:
  92. return x + y
  93. if h == 1:
  94. return -x + y
  95. if h == 2:
  96. return x - y
  97. if h == 3:
  98. return -x - y
  99. if h == 4:
  100. return x
  101. if h == 5:
  102. return -x
  103. if h == 6:
  104. return y
  105. return -y
  106.  
  107. def noise(self, x: float, y: float) -> float:
  108. xi = math.floor(x) & 255
  109. yi = math.floor(y) & 255
  110. xf = x - math.floor(x)
  111. yf = y - math.floor(y)
  112. u = self._fade(xf)
  113. v = self._fade(yf)
  114.  
  115. aa = self.perm[self.perm[xi] + yi]
  116. ab = self.perm[self.perm[xi] + yi + 1]
  117. ba = self.perm[self.perm[xi + 1] + yi]
  118. bb = self.perm[self.perm[xi + 1] + yi + 1]
  119.  
  120. x1 = self._lerp(self._grad(aa, xf, yf), self._grad(ba, xf - 1.0, yf), u)
  121. x2 = self._lerp(self._grad(ab, xf, yf - 1.0), self._grad(bb, xf - 1.0, yf - 1.0), u)
  122. return self._lerp(x1, x2, v)
  123.  
  124.  
  125. @dataclass
  126. class Slider:
  127. label: str
  128. min_value: float
  129. max_value: float
  130. value: float
  131. rect: pygame.Rect
  132. decimals: int = 2
  133. dragging: bool = False
  134. display_value: float | None = None
  135.  
  136. def _set_from_x(self, x: int) -> None:
  137. t = (x - self.rect.left) / self.rect.width
  138. t = max(0.0, min(1.0, t))
  139. self.value = self.min_value + t * (self.max_value - self.min_value)
  140.  
  141. def handle_event(self, event: pygame.event.Event) -> None:
  142. if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and self.rect.collidepoint(event.pos):
  143. self.dragging = True
  144. self._set_from_x(event.pos[0])
  145. elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
  146. self.dragging = False
  147. elif event.type == pygame.MOUSEMOTION and self.dragging:
  148. self._set_from_x(event.pos[0])
  149.  
  150. def draw(self, surface: pygame.Surface, font: pygame.font.Font) -> None:
  151. pygame.draw.rect(surface, TRACK, self.rect, border_radius=5)
  152.  
  153. shown_value = self.display_value if self.display_value is not None else self.value
  154. t = (shown_value - self.min_value) / (self.max_value - self.min_value)
  155. fill_width = int(self.rect.width * t)
  156. if fill_width > 0:
  157. pygame.draw.rect(
  158. surface,
  159. FILL,
  160. (self.rect.left, self.rect.top, fill_width, self.rect.height),
  161. border_radius=5,
  162. )
  163.  
  164. knob_x = self.rect.left + fill_width
  165. pygame.draw.circle(surface, KNOB, (knob_x, self.rect.centery), self.rect.height // 2 + 3)
  166.  
  167. label = f"{self.label}: {shown_value:.{self.decimals}f}"
  168. text_surface = font.render(label, True, TEXT)
  169. surface.blit(text_surface, (self.rect.left, self.rect.top - 20))
  170.  
  171.  
  172. def sample_profile(
  173. noise: Perlin2D, t: float, speed: float, amplitude: float, scale: float, count: int
  174. ) -> list[float]:
  175. values = []
  176. spatial_freq = 0.23
  177. scroll_y = t * speed
  178. for i in range(count):
  179. n = noise.noise(i * spatial_freq * scale, scroll_y)
  180. values.append(max(-1.0, min(1.0, n * amplitude)))
  181. return values
  182.  
  183.  
  184. def draw_grid(surface: pygame.Surface, rect: pygame.Rect, x_values: list[float], y_values: list[float]) -> None:
  185. _ = x_values, y_values
  186. pygame.draw.rect(surface, (0, 0, 0), rect)
  187.  
  188.  
  189. def advect_axes_and_dim(
  190. grid_surface: pygame.Surface, x_values: list[float], y_values: list[float], fade_factor: float
  191. ) -> None:
  192. src = grid_surface.copy()
  193. rows_shifted = pygame.Surface((GRID_SIZE, GRID_SIZE))
  194.  
  195. # Pass 1: Horizontal shift per row based on Y-axis noise.
  196. for y in range(GRID_SIZE):
  197. shift = y_values[y] * ROW_SHIFT_PIXELS
  198. for x in range(GRID_SIZE):
  199. sample_x = (x - shift) % GRID_SIZE
  200. x0 = int(math.floor(sample_x)) % GRID_SIZE
  201. x1 = (x0 + 1) % GRID_SIZE
  202. frac = sample_x - math.floor(sample_x)
  203.  
  204. c0 = src.get_at((x0, y))
  205. c1 = src.get_at((x1, y))
  206. r = (c0.r * (1.0 - frac)) + (c1.r * frac)
  207. g = (c0.g * (1.0 - frac)) + (c1.g * frac)
  208. b = (c0.b * (1.0 - frac)) + (c1.b * frac)
  209. rows_shifted.set_at((x, y), (int(r), int(g), int(b)))
  210.  
  211. # Pass 2: Vertical shift per column based on X-axis noise, then dim.
  212. for x in range(GRID_SIZE):
  213. shift = x_values[x] * COL_SHIFT_PIXELS
  214. for y in range(GRID_SIZE):
  215. sample_y = (y - shift) % GRID_SIZE
  216. y0 = int(math.floor(sample_y)) % GRID_SIZE
  217. y1 = (y0 + 1) % GRID_SIZE
  218. frac = sample_y - math.floor(sample_y)
  219.  
  220. c0 = rows_shifted.get_at((x, y0))
  221. c1 = rows_shifted.get_at((x, y1))
  222.  
  223. r = ((c0.r * (1.0 - frac)) + (c1.r * frac)) * fade_factor
  224. g = ((c0.g * (1.0 - frac)) + (c1.g * frac)) * fade_factor
  225. b = ((c0.b * (1.0 - frac)) + (c1.b * frac)) * fade_factor
  226.  
  227. grid_surface.set_at((x, y), (int(r), int(g), int(b)))
  228.  
  229.  
  230. def draw_x_graph(surface: pygame.Surface, rect: pygame.Rect, values: list[float], amplitude: float, title: str, font: pygame.font.Font) -> None:
  231. pygame.draw.rect(surface, PANEL, rect, border_radius=10)
  232. for i in range(GRID_SIZE):
  233. x = rect.left + 12 + i * ((rect.width - 24) / (GRID_SIZE - 1))
  234. pygame.draw.line(surface, GRAPH_GRID, (x, rect.top + 12), (x, rect.bottom - 12), 1)
  235.  
  236. center_y = rect.centery
  237. pygame.draw.line(surface, AXIS_COLOR, (rect.left + 12, center_y), (rect.right - 12, center_y), 2)
  238.  
  239. if len(values) < 2:
  240. return
  241.  
  242. amp_px = max(6, int((rect.height * 0.42) * min(1.0, amplitude)))
  243. points = []
  244. for i, val in enumerate(values):
  245. x = rect.left + 12 + i * ((rect.width - 24) / (len(values) - 1))
  246. y = center_y + val * amp_px
  247. points.append((x, y))
  248. pygame.draw.lines(surface, GRAPH_X_COLOR, False, points, 3)
  249.  
  250. surface.blit(font.render(title, True, TEXT), (rect.left + 12, rect.top + 8))
  251.  
  252.  
  253. def draw_y_graph(surface: pygame.Surface, rect: pygame.Rect, values: list[float], amplitude: float, title: str, font: pygame.font.Font) -> None:
  254. pygame.draw.rect(surface, PANEL, rect, border_radius=10)
  255. for i in range(GRID_SIZE):
  256. y = rect.top + 12 + i * ((rect.height - 24) / (GRID_SIZE - 1))
  257. pygame.draw.line(surface, GRAPH_GRID, (rect.left + 12, y), (rect.right - 12, y), 1)
  258.  
  259. center_x = rect.centerx
  260. pygame.draw.line(surface, AXIS_COLOR, (center_x, rect.top + 12), (center_x, rect.bottom - 12), 2)
  261.  
  262. if len(values) < 2:
  263. return
  264.  
  265. amp_px = max(6, int((rect.width * 0.42) * min(1.0, amplitude)))
  266. points = []
  267. for i, val in enumerate(values):
  268. y = rect.top + 12 + i * ((rect.height - 24) / (len(values) - 1))
  269. x = center_x + val * amp_px
  270. points.append((x, y))
  271. pygame.draw.lines(surface, GRAPH_Y_COLOR, False, points, 3)
  272.  
  273. surface.blit(font.render(title, True, TEXT), (rect.left + 12, rect.top + 8))
  274.  
  275.  
  276. def rainbow_color_with_phase(t: float, speed: float, phase: float) -> tuple[int, int, int]:
  277. hue = (t * speed + phase) % 1.0
  278. r, g, b = colorsys.hsv_to_rgb(hue, 1.0, 1.0)
  279. return int(r * 255), int(g * 255), int(b * 255)
  280.  
  281.  
  282. def blend_subpixel(grid_surface: pygame.Surface, x: float, y: float, color: tuple[int, int, int]) -> None:
  283. x0 = int(math.floor(x))
  284. y0 = int(math.floor(y))
  285. fx = x - x0
  286. fy = y - y0
  287.  
  288. for ox, wx in ((0, 1.0 - fx), (1, fx)):
  289. for oy, wy in ((0, 1.0 - fy), (1, fy)):
  290. px = x0 + ox
  291. py = y0 + oy
  292. if not (0 <= px < GRID_SIZE and 0 <= py < GRID_SIZE):
  293. continue
  294. w = wx * wy
  295. if w <= 0.0:
  296. continue
  297. old = grid_surface.get_at((px, py))
  298. nr = int(old.r * (1.0 - w) + color[0] * w)
  299. ng = int(old.g * (1.0 - w) + color[1] * w)
  300. nb = int(old.b * (1.0 - w) + color[2] * w)
  301. grid_surface.set_at((px, py), (nr, ng, nb))
  302.  
  303.  
  304. def blend_pixel_weighted(grid_surface: pygame.Surface, px: int, py: int, color: tuple[int, int, int], w: float) -> None:
  305. if not (0 <= px < GRID_SIZE and 0 <= py < GRID_SIZE):
  306. return
  307. w = max(0.0, min(1.0, w))
  308. if w <= 0.0:
  309. return
  310. old = grid_surface.get_at((px, py))
  311. nr = int(old.r * (1.0 - w) + color[0] * w)
  312. ng = int(old.g * (1.0 - w) + color[1] * w)
  313. nb = int(old.b * (1.0 - w) + color[2] * w)
  314. grid_surface.set_at((px, py), (nr, ng, nb))
  315.  
  316.  
  317. def draw_aa_endpoint_disc(grid_surface: pygame.Surface, cx: float, cy: float, color: tuple[int, int, int], radius: float = 0.75) -> None:
  318. min_x = max(0, int(math.floor(cx - radius - 1.0)))
  319. max_x = min(GRID_SIZE - 1, int(math.ceil(cx + radius + 1.0)))
  320. min_y = max(0, int(math.floor(cy - radius - 1.0)))
  321. max_y = min(GRID_SIZE - 1, int(math.ceil(cy + radius + 1.0)))
  322. for py in range(min_y, max_y + 1):
  323. for px in range(min_x, max_x + 1):
  324. dx = (px + 0.5) - cx
  325. dy = (py + 0.5) - cy
  326. dist = math.hypot(dx, dy)
  327. # 1px soft edge for antialiasing.
  328. w = max(0.0, min(1.0, radius + 0.5 - dist))
  329. blend_pixel_weighted(grid_surface, px, py, color, w)
  330.  
  331.  
  332. def draw_aa_subpixel_line(
  333. grid_surface: pygame.Surface,
  334. x0: float,
  335. y0: float,
  336. x1: float,
  337. y1: float,
  338. t: float,
  339. color_shift: float,
  340. ) -> None:
  341. dx = x1 - x0
  342. dy = y1 - y0
  343. steps = max(1, int(max(abs(dx), abs(dy)) * 3))
  344. for i in range(steps + 1):
  345. u = i / steps
  346. x = x0 + dx * u
  347. y = y0 + dy * u
  348. xi = math.floor(x)
  349. yi = math.floor(y)
  350. fx = x - xi
  351. fy = y - yi
  352. color = rainbow_color_with_phase(t, color_shift, u)
  353.  
  354. blend_pixel_weighted(grid_surface, int(xi), int(yi), color, (1.0 - fx) * (1.0 - fy))
  355. blend_pixel_weighted(grid_surface, int(xi + 1), int(yi), color, fx * (1.0 - fy))
  356. blend_pixel_weighted(grid_surface, int(xi), int(yi + 1), color, (1.0 - fx) * fy)
  357. blend_pixel_weighted(grid_surface, int(xi + 1), int(yi + 1), color, fx * fy)
  358.  
  359.  
  360. def inject_rainbow_two_rect_lines(grid_surface: pygame.Surface, t: float, color_shift: float) -> None:
  361. line_points = []
  362. # Full outer rectangle traversal
  363. for x in range(GRID_SIZE):
  364. line_points.append((x, 0))
  365. for y in range(1, GRID_SIZE):
  366. line_points.append((GRID_SIZE - 1, y))
  367. for x in range(GRID_SIZE - 2, -1, -1):
  368. line_points.append((x, GRID_SIZE - 1))
  369. for y in range(1, GRID_SIZE):
  370. line_points.append((0, GRID_SIZE - 1 - y))
  371.  
  372. total = len(line_points)
  373. for i, (x, y) in enumerate(line_points):
  374. color = rainbow_color_with_phase(t, color_shift, i / total)
  375. grid_surface.set_at((x, y), color)
  376.  
  377.  
  378. def main() -> None:
  379. pygame.init()
  380. screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
  381. pygame.display.set_caption("Perlin Grid Visualisierung")
  382. clock = pygame.time.Clock()
  383.  
  384. font = pygame.font.SysFont("arial", 18)
  385. small_font = pygame.font.SysFont("arial", 15)
  386.  
  387. grid_rect = pygame.Rect(MARGIN + LEFT_GRAPH_WIDTH + MARGIN, MARGIN, GRID_PIXELS, GRID_PIXELS)
  388. x_graph_rect = pygame.Rect(grid_rect.left, grid_rect.bottom + MARGIN, GRID_PIXELS, BOTTOM_GRAPH_HEIGHT)
  389. y_graph_rect = pygame.Rect(MARGIN, grid_rect.top, LEFT_GRAPH_WIDTH, GRID_PIXELS)
  390. grid_surface = pygame.Surface((GRID_SIZE, GRID_SIZE))
  391. grid_surface.fill((0, 0, 0))
  392.  
  393. ui_x = grid_rect.right + MARGIN
  394. panel_rect = pygame.Rect(ui_x, MARGIN, RIGHT_UI_WIDTH, WINDOW_HEIGHT - 2 * MARGIN)
  395. slider_y = lambda idx: MARGIN + UI_SLIDER_START_Y + idx * UI_SLIDER_GAP_Y
  396.  
  397. sliders = [
  398. Slider("X Speed", -2.00, 2.00, 0.10, pygame.Rect(ui_x + 20, slider_y(0), RIGHT_UI_WIDTH - 40, 14), 2),
  399. Slider("X Amplitude", 0.10, 1.00, 0.75, pygame.Rect(ui_x + 20, slider_y(1), RIGHT_UI_WIDTH - 40, 14), 2),
  400. Slider("X Frequency", 0.10, 4.00, 0.33, pygame.Rect(ui_x + 20, slider_y(2), RIGHT_UI_WIDTH - 40, 14), 2),
  401. Slider("Y Speed", -2.00, 2.00, 0.10, pygame.Rect(ui_x + 20, slider_y(3), RIGHT_UI_WIDTH - 40, 14), 2),
  402. Slider("Y Amplitude", 0.10, 1.00, 0.75, pygame.Rect(ui_x + 20, slider_y(4), RIGHT_UI_WIDTH - 40, 14), 2),
  403. Slider("Y Frequency", 0.10, 4.00, 0.32, pygame.Rect(ui_x + 20, slider_y(5), RIGHT_UI_WIDTH - 40, 14), 2),
  404. Slider("Variation Intensity", 0.00, 4.00, 4.00, pygame.Rect(ui_x + 20, slider_y(6), RIGHT_UI_WIDTH - 40, 14), 2),
  405. Slider("Variation Speed", 0.00, 4.00, VARIATION_SPEED_DEFAULT, pygame.Rect(ui_x + 20, slider_y(7), RIGHT_UI_WIDTH - 40, 14), 2),
  406. Slider("Color Shift", 0.00, 1.00, COLOR_SHIFT_DEFAULT, pygame.Rect(ui_x + 20, slider_y(8), RIGHT_UI_WIDTH - 40, 14), 2),
  407. Slider("Fade %", 90.0, 99.999, 99.922, pygame.Rect(ui_x + 20, slider_y(9), RIGHT_UI_WIDTH - 40, 14), 3),
  408. ]
  409.  
  410. noise_x = Perlin2D(seed=42)
  411. noise_y = Perlin2D(seed=1337)
  412. amp_var_x = Perlin1D(seed=101)
  413. amp_var_y = Perlin1D(seed=202)
  414.  
  415. start_time = pygame.time.get_ticks() / 1000.0
  416. running = True
  417. while running:
  418. dt = clock.tick(FPS) / 1000.0
  419. _ = dt
  420.  
  421. for event in pygame.event.get():
  422. if event.type == pygame.QUIT:
  423. running = False
  424. for slider in sliders:
  425. slider.handle_event(event)
  426.  
  427. t = pygame.time.get_ticks() / 1000.0 - start_time
  428.  
  429. x_speed, x_amp, x_scale, y_speed, y_amp, y_scale, variation_intensity, variation_speed, color_shift, fade_percent = [s.value for s in sliders]
  430.  
  431. # Two independent slow 1D noises drive amplitude variation.
  432. n_var_x = amp_var_x.noise(t * 0.16 * variation_speed)
  433. n_var_y = amp_var_y.noise(t * 0.13 * variation_speed + 17.0)
  434.  
  435. # Variation modulates itself using the same random drivers.
  436. self_mod = 0.5 + 0.5 * ((n_var_x + n_var_y) * 0.5)
  437. eff_variation = variation_intensity * self_mod
  438.  
  439. x_amp_eff = clamp(x_amp + (n_var_x * 0.45 * eff_variation), 0.10, 1.00)
  440. y_amp_eff = clamp(y_amp + (n_var_y * 0.45 * eff_variation), 0.10, 1.00)
  441.  
  442. # Show live effective amplitude values in the slider labels.
  443. sliders[1].display_value = x_amp_eff
  444. sliders[4].display_value = y_amp_eff
  445.  
  446. x_profile = sample_profile(noise_x, t, x_speed, x_amp_eff, x_scale, GRID_SIZE)
  447. x_profile = list(reversed(x_profile))
  448. y_profile = sample_profile(noise_y, t, y_speed, y_amp_eff, y_scale, GRID_SIZE)
  449. fade_factor = fade_percent / 100.0
  450.  
  451. inject_rainbow_two_rect_lines(grid_surface, t, color_shift)
  452. advect_axes_and_dim(grid_surface, x_profile, y_profile, fade_factor)
  453.  
  454. screen.fill(BG)
  455. draw_grid(screen, grid_rect, x_profile, y_profile)
  456. scaled_grid = pygame.transform.scale(grid_surface, (GRID_PIXELS, GRID_PIXELS))
  457. screen.blit(scaled_grid, grid_rect.topleft)
  458. draw_x_graph(screen, x_graph_rect, x_profile, x_amp_eff, "x controls columns", font)
  459. draw_y_graph(screen, y_graph_rect, y_profile, y_amp_eff, "y controls rows", font)
  460.  
  461. pygame.draw.rect(screen, PANEL, panel_rect, border_radius=10)
  462. title = font.render("Control Parameter", True, TEXT)
  463. screen.blit(title, (panel_rect.left + 20, panel_rect.top + 20))
  464.  
  465. for slider in sliders:
  466. slider.draw(screen, font)
  467.  
  468. pygame.display.flip()
  469.  
  470. pygame.quit()
  471. sys.exit(0)
  472.  
  473.  
  474. if __name__ == "__main__":
  475. main()
  476.  
Advertisement
Add Comment
Please, Sign In to add comment