StePe

Formulas V1.2

Mar 20th, 2026
100
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 46.23 KB | None | 0 0
  1. import colorsys
  2. import math
  3. import sys
  4. from dataclasses import dataclass
  5.  
  6. import pygame
  7.  
  8. GRID_SIZE = 32
  9. CELL_SIZE = 16
  10. GRID_PIXELS = GRID_SIZE * CELL_SIZE
  11. RIGHT_UI_WIDTH = 330
  12. MARGIN = 20
  13. FPS = 60
  14.  
  15. COLOR_SHIFT_DEFAULT = 0.10
  16. ENDPOINT_SPEED_DEFAULT = 0.35
  17. SPIRAL_TRANSPORT_FRACTION = 0.45
  18. SPIRAL_DIM_DEFAULT = 1.00
  19. ROUND_SPIRAL_ANGULAR_STEP = 0.28
  20. ROUND_SPIRAL_RADIAL_STEP = 0.18
  21. ORBIT_DOT_DIAMETER = 2.5
  22. ORBIT_RADIUS = 9.5
  23.  
  24. WINDOW_WIDTH = MARGIN * 3 + GRID_PIXELS + RIGHT_UI_WIDTH
  25. WINDOW_HEIGHT = MARGIN * 2 + GRID_PIXELS
  26.  
  27. BG = (15, 18, 24)
  28. TEXT = (230, 230, 240)
  29. TRACK = (50, 56, 70)
  30. FILL = (70, 130, 255)
  31. KNOB = (235, 239, 248)
  32. PANEL = (22, 26, 36)
  33. BORDER = (70, 70, 85)
  34.  
  35. MODE_NAMES = [
  36.     "No Trail",
  37.     "2. Chromatic Flow Split",
  38.     "4. Polar Warp Pulsation",
  39.     "4b. Polar Warp 2",
  40.     "6. Shockwave Displacement",
  41.     "6b. Shockwave 2",
  42.     "7. Rotating Coordinate",
  43.     "7b. Rotating Wind",
  44.     "7c. Rotating Wind Wave",
  45.     "Meandering Jet Field",
  46.     "10. Attractor Fields",
  47.     "15. Fractal Spiral Step",
  48.     "Ring Flow",
  49.     "Square Spiral Stream",
  50.     "Spiral Stream",
  51.     "Spiral Outwards",
  52.     "To the center",
  53.     "From the center",
  54.     "Directional Noise",
  55.     "Wind + Noise Alpha Mask",
  56. ]
  57.  
  58. MODE_BUTTON_LABELS = [
  59.     "No Trail",
  60.     "Chrom Split",
  61.     "Polar Warp",
  62.     "Polar Warp 2",
  63.     "Shockwave",
  64.     "Shockwave 2",
  65.     "Rotate Coord",
  66.     "Rotating Wind",
  67.     "Rotating Wind Wave",
  68.     "Meandering Jet",
  69.     "Attractors",
  70.     "Fractal Spiral",
  71.     "Ring Flow",
  72.     "Square Spiral",
  73.     "Spiral In",
  74.     "Spiral Out",
  75.     "To Center",
  76.     "From Center",
  77.     "Directional Noise",
  78.     "Wind + Noise Alpha Mask",
  79. ]
  80.  
  81.  
  82. @dataclass
  83. class Slider:
  84.     label: str
  85.     min_value: float
  86.     max_value: float
  87.     value: float
  88.     rect: pygame.Rect
  89.     decimals: int = 2
  90.     dragging: bool = False
  91.  
  92.     def _set_from_x(self, x: int) -> None:
  93.         t = (x - self.rect.left) / self.rect.width
  94.         t = max(0.0, min(1.0, t))
  95.         self.value = self.min_value + t * (self.max_value - self.min_value)
  96.  
  97.     def handle_event(self, event: pygame.event.Event) -> None:
  98.         if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and self.rect.collidepoint(event.pos):
  99.             self.dragging = True
  100.             self._set_from_x(event.pos[0])
  101.         elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
  102.             self.dragging = False
  103.         elif event.type == pygame.MOUSEMOTION and self.dragging:
  104.             self._set_from_x(event.pos[0])
  105.  
  106.     def draw(self, surface: pygame.Surface, font: pygame.font.Font) -> None:
  107.         pygame.draw.rect(surface, TRACK, self.rect, border_radius=5)
  108.         t = (self.value - self.min_value) / (self.max_value - self.min_value)
  109.         fill_width = int(self.rect.width * t)
  110.         if fill_width > 0:
  111.             pygame.draw.rect(surface, FILL, (self.rect.left, self.rect.top, fill_width, self.rect.height), border_radius=5)
  112.         knob_x = self.rect.left + fill_width
  113.         pygame.draw.circle(surface, KNOB, (knob_x, self.rect.centery), self.rect.height // 2 + 3)
  114.         label = f"{self.label}: {self.value:.{self.decimals}f}"
  115.         surface.blit(font.render(label, True, TEXT), (self.rect.left, self.rect.top - 20))
  116.  
  117.  
  118. @dataclass
  119. class Button:
  120.     label: str
  121.     rect: pygame.Rect
  122.     active: bool = False
  123.  
  124.     def handle_event(self, event: pygame.event.Event) -> bool:
  125.         return event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and self.rect.collidepoint(event.pos)
  126.  
  127.     def draw(self, surface: pygame.Surface, font: pygame.font.Font) -> None:
  128.         fill = (74, 124, 189) if self.active else (52, 52, 62)
  129.         pygame.draw.rect(surface, fill, self.rect, border_radius=6)
  130.         pygame.draw.rect(surface, BORDER, self.rect, 1, border_radius=6)
  131.         text = font.render(self.label, True, TEXT)
  132.         surface.blit(text, (self.rect.centerx - text.get_width() // 2, self.rect.centery - text.get_height() // 2))
  133.  
  134.  
  135. def clamp(v: float, lo: float, hi: float) -> float:
  136.     return max(lo, min(hi, v))
  137.  
  138.  
  139. def hsv_color_with_phase(t: float, speed: float, phase: float) -> tuple[int, int, int]:
  140.     hue = (t * speed + phase) % 1.0
  141.     r, g, b = colorsys.hsv_to_rgb(hue, 1.0, 1.0)
  142.     return int(r * 255), int(g * 255), int(b * 255)
  143.  
  144.  
  145. def get_rgb(surface: pygame.Surface, x: int, y: int) -> tuple[int, int, int]:
  146.     if 0 <= x < GRID_SIZE and 0 <= y < GRID_SIZE:
  147.         c = surface.get_at((x, y))
  148.         return c.r, c.g, c.b
  149.     return 0, 0, 0
  150.  
  151.  
  152. def set_rgb(surface: pygame.Surface, x: int, y: int, color: tuple[int, int, int]) -> None:
  153.     if 0 <= x < GRID_SIZE and 0 <= y < GRID_SIZE:
  154.         surface.set_at((x, y), color)
  155.  
  156.  
  157. def sample_rgb_bilinear(surface: pygame.Surface, x: float, y: float) -> tuple[float, float, float]:
  158.     x = clamp(x, 0.0, (GRID_SIZE - 1) - 1e-6)
  159.     y = clamp(y, 0.0, (GRID_SIZE - 1) - 1e-6)
  160.  
  161.     x0 = int(math.floor(x))
  162.     y0 = int(math.floor(y))
  163.     x1 = min(GRID_SIZE - 1, x0 + 1)
  164.     y1 = min(GRID_SIZE - 1, y0 + 1)
  165.     fx = x - x0
  166.     fy = y - y0
  167.  
  168.     c00 = get_rgb(surface, x0, y0)
  169.     c10 = get_rgb(surface, x1, y0)
  170.     c01 = get_rgb(surface, x0, y1)
  171.     c11 = get_rgb(surface, x1, y1)
  172.  
  173.     r0 = c00[0] * (1.0 - fx) + c10[0] * fx
  174.     g0 = c00[1] * (1.0 - fx) + c10[1] * fx
  175.     b0 = c00[2] * (1.0 - fx) + c10[2] * fx
  176.  
  177.     r1 = c01[0] * (1.0 - fx) + c11[0] * fx
  178.     g1 = c01[1] * (1.0 - fx) + c11[1] * fx
  179.     b1 = c01[2] * (1.0 - fx) + c11[2] * fx
  180.  
  181.     return (
  182.         r0 * (1.0 - fy) + r1 * fy,
  183.         g0 * (1.0 - fy) + g1 * fy,
  184.         b0 * (1.0 - fy) + b1 * fy,
  185.     )
  186.  
  187.  
  188. def transport_and_dim(
  189.     surface: pygame.Surface,
  190.     x: int,
  191.     y: int,
  192.     sx: int,
  193.     sy: int,
  194.     fraction: float,
  195.     dim: float,
  196. ) -> None:
  197.     c0 = get_rgb(surface, x, y)
  198.     c1 = get_rgb(surface, sx, sy)
  199.     r = c0[0] * (1.0 - fraction) + c1[0] * fraction
  200.     g = c0[1] * (1.0 - fraction) + c1[1] * fraction
  201.     b = c0[2] * (1.0 - fraction) + c1[2] * fraction
  202.     set_rgb(surface, x, y, (int(round(r * dim)), int(round(g * dim)), int(round(b * dim))))
  203.  
  204.  
  205. def apply_square_spiral_tail(
  206.     surface: pygame.Surface, cx: int, cy: int, radius: int, fraction: float, dim: float
  207. ) -> None:
  208.     for d in range(radius, -1, -1):
  209.         for i in range(cx - d, cx + d + 1):
  210.             transport_and_dim(surface, i, cy - d, i + 1, cy - d, fraction, dim)
  211.         for i in range(cy - d, cy + d + 1):
  212.             transport_and_dim(surface, cx + d, i, cx + d, i + 1, fraction, dim)
  213.         for i in range(cx + d, cx - d - 1, -1):
  214.             transport_and_dim(surface, i, cy + d, i - 1, cy + d, fraction, dim)
  215.         for i in range(cy + d, cy - d - 1, -1):
  216.             transport_and_dim(surface, cx - d, i, cx - d, i - 1, fraction, dim)
  217.  
  218.  
  219. def apply_round_spiral_tail(
  220.     surface: pygame.Surface,
  221.     cx: float,
  222.     cy: float,
  223.     radius: float,
  224.     fraction: float,
  225.     dim: float,
  226.     outward: bool = False,
  227. ) -> None:
  228.     src = surface.copy()
  229.     for y in range(GRID_SIZE):
  230.         for x in range(GRID_SIZE):
  231.             dx = x - cx
  232.             dy = y - cy
  233.             r = math.hypot(dx, dy)
  234.             if r > radius:
  235.                 c = get_rgb(src, x, y)
  236.                 set_rgb(surface, x, y, (int(round(c[0] * dim)), int(round(c[1] * dim)), int(round(c[2] * dim))))
  237.                 continue
  238.  
  239.             theta = math.atan2(dy, dx)
  240.             if outward:
  241.                 sample_r = max(0.0, r - ROUND_SPIRAL_RADIAL_STEP)
  242.             else:
  243.                 sample_r = min(radius + 1.5, r + ROUND_SPIRAL_RADIAL_STEP)
  244.             sample_theta = theta - ROUND_SPIRAL_ANGULAR_STEP
  245.             sx = cx + math.cos(sample_theta) * sample_r
  246.             sy = cy + math.sin(sample_theta) * sample_r
  247.  
  248.             c0 = get_rgb(src, x, y)
  249.             c1 = sample_rgb_bilinear(src, sx, sy)
  250.             nr = (c0[0] * (1.0 - fraction) + c1[0] * fraction) * dim
  251.             ng = (c0[1] * (1.0 - fraction) + c1[1] * fraction) * dim
  252.             nb = (c0[2] * (1.0 - fraction) + c1[2] * fraction) * dim
  253.             set_rgb(surface, x, y, (int(round(nr)), int(round(ng)), int(round(nb))))
  254.  
  255.  
  256. def apply_to_center_tail(surface: pygame.Surface, cx: float, cy: float, fraction: float, dim: float) -> None:
  257.     src = surface.copy()
  258.     for y in range(GRID_SIZE):
  259.         for x in range(GRID_SIZE):
  260.             dx = x - cx
  261.             dy = y - cy
  262.             r = math.hypot(dx, dy)
  263.             if r > 1e-6:
  264.                 ux = dx / r
  265.                 uy = dy / r
  266.                 sx = x + ux * ROUND_SPIRAL_RADIAL_STEP
  267.                 sy = y + uy * ROUND_SPIRAL_RADIAL_STEP
  268.                 c1 = sample_rgb_bilinear(src, sx, sy)
  269.             else:
  270.                 c1 = get_rgb(src, x, y)
  271.             c0 = get_rgb(src, x, y)
  272.             nr = (c0[0] * (1.0 - fraction) + c1[0] * fraction) * dim
  273.             ng = (c0[1] * (1.0 - fraction) + c1[1] * fraction) * dim
  274.             nb = (c0[2] * (1.0 - fraction) + c1[2] * fraction) * dim
  275.             set_rgb(surface, x, y, (int(round(nr)), int(round(ng)), int(round(nb))))
  276.  
  277.  
  278. def apply_from_center_tail(surface: pygame.Surface, cx: float, cy: float, fraction: float, dim: float) -> None:
  279.     src = surface.copy()
  280.     for y in range(GRID_SIZE):
  281.         for x in range(GRID_SIZE):
  282.             dx = x - cx
  283.             dy = y - cy
  284.             r = math.hypot(dx, dy)
  285.             if r > 1e-6:
  286.                 ux = dx / r
  287.                 uy = dy / r
  288.                 sx = x - ux * ROUND_SPIRAL_RADIAL_STEP
  289.                 sy = y - uy * ROUND_SPIRAL_RADIAL_STEP
  290.                 c1 = sample_rgb_bilinear(src, sx, sy)
  291.             else:
  292.                 c1 = get_rgb(src, x, y)
  293.             c0 = get_rgb(src, x, y)
  294.             nr = (c0[0] * (1.0 - fraction) + c1[0] * fraction) * dim
  295.             ng = (c0[1] * (1.0 - fraction) + c1[1] * fraction) * dim
  296.             nb = (c0[2] * (1.0 - fraction) + c1[2] * fraction) * dim
  297.             set_rgb(surface, x, y, (int(round(nr)), int(round(ng)), int(round(nb))))
  298.  
  299.  
  300. def blend_pixel_weighted(surface: pygame.Surface, px: int, py: int, color: tuple[int, int, int], w: float) -> None:
  301.     if not (0 <= px < GRID_SIZE and 0 <= py < GRID_SIZE):
  302.         return
  303.     w = clamp(w, 0.0, 1.0)
  304.     if w <= 0.0:
  305.         return
  306.     old = surface.get_at((px, py))
  307.     nr = int(old.r * (1.0 - w) + color[0] * w)
  308.     ng = int(old.g * (1.0 - w) + color[1] * w)
  309.     nb = int(old.b * (1.0 - w) + color[2] * w)
  310.     surface.set_at((px, py), (nr, ng, nb))
  311.  
  312.  
  313. def draw_aa_endpoint_disc(surface: pygame.Surface, cx: float, cy: float, color: tuple[int, int, int], radius: float = 0.85) -> None:
  314.     min_x = max(0, int(math.floor(cx - radius - 1.0)))
  315.     max_x = min(GRID_SIZE - 1, int(math.ceil(cx + radius + 1.0)))
  316.     min_y = max(0, int(math.floor(cy - radius - 1.0)))
  317.     max_y = min(GRID_SIZE - 1, int(math.ceil(cy + radius + 1.0)))
  318.     for py in range(min_y, max_y + 1):
  319.         for px in range(min_x, max_x + 1):
  320.             dx = (px + 0.5) - cx
  321.             dy = (py + 0.5) - cy
  322.             dist = math.hypot(dx, dy)
  323.             w = clamp(radius + 0.5 - dist, 0.0, 1.0)
  324.             blend_pixel_weighted(surface, px, py, color, w)
  325.  
  326.  
  327. def draw_aa_subpixel_ring(
  328.     surface: pygame.Surface,
  329.     cx: float,
  330.     cy: float,
  331.     color: tuple[int, int, int],
  332.     radius: float = 2.5,
  333.     thickness: float = 1.8,
  334. ) -> None:
  335.     half_t = thickness * 0.5
  336.     inner = max(0.0, radius - half_t)
  337.     outer = radius + half_t
  338.     min_x = max(0, int(math.floor(cx - outer - 1.0)))
  339.     max_x = min(GRID_SIZE - 1, int(math.ceil(cx + outer + 1.0)))
  340.     min_y = max(0, int(math.floor(cy - outer - 1.0)))
  341.     max_y = min(GRID_SIZE - 1, int(math.ceil(cy + outer + 1.0)))
  342.     for py in range(min_y, max_y + 1):
  343.         for px in range(min_x, max_x + 1):
  344.             dx = (px + 0.5) - cx
  345.             dy = (py + 0.5) - cy
  346.             dist = math.hypot(dx, dy)
  347.             # Antialiased annulus coverage with ~1px smooth boundary.
  348.             in_w = clamp(dist - inner + 0.5, 0.0, 1.0)
  349.             out_w = clamp(outer - dist + 0.5, 0.0, 1.0)
  350.             w = in_w * out_w
  351.             blend_pixel_weighted(surface, px, py, color, w)
  352.  
  353.  
  354. def draw_aa_subpixel_rainbow_ring(
  355.     surface: pygame.Surface,
  356.     cx: float,
  357.     cy: float,
  358.     t: float,
  359.     color_shift: float,
  360.     phase_dir: float = 1.0,
  361.     radius: float = 5.625,
  362.     thickness: float = 1.8,
  363. ) -> None:
  364.     half_t = thickness * 0.5
  365.     inner = max(0.0, radius - half_t)
  366.     outer = radius + half_t
  367.     min_x = max(0, int(math.floor(cx - outer - 1.0)))
  368.     max_x = min(GRID_SIZE - 1, int(math.ceil(cx + outer + 1.0)))
  369.     min_y = max(0, int(math.floor(cy - outer - 1.0)))
  370.     max_y = min(GRID_SIZE - 1, int(math.ceil(cy + outer + 1.0)))
  371.     for py in range(min_y, max_y + 1):
  372.         for px in range(min_x, max_x + 1):
  373.             dx = (px + 0.5) - cx
  374.             dy = (py + 0.5) - cy
  375.             dist = math.hypot(dx, dy)
  376.             in_w = clamp(dist - inner + 0.5, 0.0, 1.0)
  377.             out_w = clamp(outer - dist + 0.5, 0.0, 1.0)
  378.             w = in_w * out_w
  379.             if w <= 0.0:
  380.                 continue
  381.             angle_norm = (math.atan2(dy, dx) / (2.0 * math.pi)) % 1.0
  382.             color = hsv_color_with_phase(t, color_shift * phase_dir, angle_norm)
  383.             blend_pixel_weighted(surface, px, py, color, w)
  384.  
  385.  
  386. def draw_aa_subpixel_line(
  387.     surface: pygame.Surface,
  388.     x0: float,
  389.     y0: float,
  390.     x1: float,
  391.     y1: float,
  392.     t: float,
  393.     color_shift: float,
  394.     phase_start: float = 0.0,
  395.     phase_span: float = 1.0,
  396. ) -> None:
  397.     dx = x1 - x0
  398.     dy = y1 - y0
  399.     steps = max(1, int(max(abs(dx), abs(dy)) * 3))
  400.     for i in range(steps + 1):
  401.         u = i / steps
  402.         x = x0 + dx * u
  403.         y = y0 + dy * u
  404.         xi = math.floor(x)
  405.         yi = math.floor(y)
  406.         fx = x - xi
  407.         fy = y - yi
  408.         color = hsv_color_with_phase(t, color_shift, phase_start + u * phase_span)
  409.         blend_pixel_weighted(surface, int(xi), int(yi), color, (1.0 - fx) * (1.0 - fy))
  410.         blend_pixel_weighted(surface, int(xi + 1), int(yi), color, fx * (1.0 - fy))
  411.         blend_pixel_weighted(surface, int(xi), int(yi + 1), color, (1.0 - fx) * fy)
  412.         blend_pixel_weighted(surface, int(xi + 1), int(yi + 1), color, fx * fy)
  413.  
  414.  
  415. def inject_lissajous_line(surface: pygame.Surface, t: float, color_shift: float, endpoint_speed: float) -> None:
  416.     c = (GRID_SIZE - 1) * 0.5
  417.     s = endpoint_speed
  418.     x1 = c + 11.5 * math.sin(t * s * 1.13 + 0.20)
  419.     y1 = c + 10.5 * math.sin(t * s * 1.71 + 1.30)
  420.     x2 = c + 12.0 * math.sin(t * s * 1.89 + 2.20)
  421.     y2 = c + 11.0 * math.sin(t * s * 1.37 + 0.70)
  422.     draw_aa_subpixel_line(surface, x1, y1, x2, y2, t, color_shift)
  423.  
  424.  
  425. def inject_orbiting_dots(surface: pygame.Surface, t: float, orbit_speed: float, color_shift: float) -> None:
  426.     cx = (GRID_SIZE - 1) * 0.5
  427.     cy = (GRID_SIZE - 1) * 0.5
  428.     radius = ORBIT_DOT_DIAMETER * 0.5
  429.     base_angle = t * orbit_speed
  430.     for i in range(3):
  431.         a = base_angle + i * (2.0 * math.pi / 3.0)
  432.         x = cx + math.cos(a) * ORBIT_RADIUS
  433.         y = cy + math.sin(a) * ORBIT_RADIUS
  434.         color = hsv_color_with_phase(t, color_shift, i / 3.0)
  435.         draw_aa_endpoint_disc(surface, x, y, color, radius=radius)
  436.  
  437.  
  438. def inject_lissajous_triangle(surface: pygame.Surface, t: float, color_shift: float, endpoint_speed: float) -> None:
  439.     c = (GRID_SIZE - 1) * 0.5
  440.     s = endpoint_speed
  441.     p1 = (
  442.         c + 11.8 * math.sin(t * s * 1.13 + 0.20),
  443.         c + 10.4 * math.sin(t * s * 1.71 + 1.30),
  444.     )
  445.     p2 = (
  446.         c + 11.2 * math.sin(t * s * 1.89 + 2.20),
  447.         c + 10.9 * math.sin(t * s * 1.37 + 0.70),
  448.     )
  449.     p3 = (
  450.         c + 10.9 * math.sin(t * s * 1.57 + 3.10),
  451.         c + 11.3 * math.sin(t * s * 1.21 + 2.40),
  452.     )
  453.  
  454.     l12 = math.hypot(p2[0] - p1[0], p2[1] - p1[1])
  455.     l23 = math.hypot(p3[0] - p2[0], p3[1] - p2[1])
  456.     l31 = math.hypot(p1[0] - p3[0], p1[1] - p3[1])
  457.     total = max(1e-6, l12 + l23 + l31)
  458.  
  459.     s1 = 0.0
  460.     s2 = l12 / total
  461.     s3 = (l12 + l23) / total
  462.  
  463.     draw_aa_subpixel_line(surface, p1[0], p1[1], p2[0], p2[1], t, color_shift, phase_start=s1, phase_span=(l12 / total))
  464.     draw_aa_subpixel_line(surface, p2[0], p2[1], p3[0], p3[1], t, color_shift, phase_start=s2, phase_span=(l23 / total))
  465.     draw_aa_subpixel_line(surface, p3[0], p3[1], p1[0], p1[1], t, color_shift, phase_start=s3, phase_span=(l31 / total))
  466.  
  467.  
  468. def inject_bouncing_ring(surface: pygame.Surface, t: float, color_shift: float, endpoint_speed: float) -> None:
  469.     radius = 5.625
  470.     thickness = 1.8
  471.     c = (GRID_SIZE - 1) * 0.5
  472.     outer = radius + thickness * 0.5
  473.     amp_x = max(0.0, c - outer - 0.2)
  474.     amp_y = max(0.0, c - outer - 0.2)
  475.     s = max(0.0, endpoint_speed)
  476.     x = c + amp_x * math.sin(t * s * 1.23 + 0.20)
  477.     y = c + amp_y * math.sin(t * s * 1.71 + 1.10)
  478.     draw_aa_subpixel_rainbow_ring(surface, x, y, t, color_shift, phase_dir=1.0, radius=radius, thickness=thickness)
  479.  
  480.  
  481. def scalar_noise(x: float, y: float, t: float) -> float:
  482.     n = (
  483.         math.sin(1.73 * x + 0.91 * t)
  484.         + math.sin(1.37 * y - 1.11 * t)
  485.         + math.sin(1.09 * (x + y) + 0.77 * t)
  486.     )
  487.     return n / 3.0
  488.  
  489.  
  490. def noise1d(x: float) -> float:
  491.     # Deterministic value noise in [-1, 1] with smooth interpolation.
  492.     x0 = math.floor(x)
  493.     x1 = x0 + 1
  494.     f = x - x0
  495.     u = f * f * (3.0 - 2.0 * f)
  496.  
  497.     def rnd(i: int) -> float:
  498.         v = math.sin(i * 127.1 + 311.7) * 43758.5453
  499.         return (v - math.floor(v)) * 2.0 - 1.0
  500.  
  501.     n0 = rnd(int(x0))
  502.     n1 = rnd(int(x1))
  503.     return n0 * (1.0 - u) + n1 * u
  504.  
  505.  
  506. def noise1d_hq(x: float) -> float:
  507.     # Higher-quality 1D value noise with quintic fade interpolation.
  508.     x0 = math.floor(x)
  509.     x1 = x0 + 1
  510.     f = x - x0
  511.     u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0)
  512.  
  513.     def rnd(i: int) -> float:
  514.         v = math.sin(i * 157.31 + 17.13) * 43758.5453123
  515.         return (v - math.floor(v)) * 2.0 - 1.0
  516.  
  517.     n0 = rnd(int(x0))
  518.     n1 = rnd(int(x1))
  519.     return n0 * (1.0 - u) + n1 * u
  520.  
  521.  
  522. def fbm1d_hq(x: float) -> float:
  523.     # Multi-octave fractal noise in [-1, 1].
  524.     total = 0.0
  525.     amp = 1.0
  526.     freq = 1.0
  527.     norm = 0.0
  528.     for _ in range(6):
  529.         total += noise1d_hq(x * freq) * amp
  530.         norm += amp
  531.         amp *= 0.5
  532.         freq *= 2.0
  533.     return total / max(norm, 1e-6)
  534.  
  535.  
  536. def noise2d_hq(x: float, y: float) -> float:
  537.     # Higher-quality 2D value noise in [-1, 1] with quintic fade interpolation.
  538.     x0 = math.floor(x)
  539.     y0 = math.floor(y)
  540.     x1 = x0 + 1
  541.     y1 = y0 + 1
  542.     fx = x - x0
  543.     fy = y - y0
  544.     ux = fx * fx * fx * (fx * (fx * 6.0 - 15.0) + 10.0)
  545.     uy = fy * fy * fy * (fy * (fy * 6.0 - 15.0) + 10.0)
  546.  
  547.     def rnd(ix: int, iy: int) -> float:
  548.         v = math.sin(ix * 127.1 + iy * 311.7 + 74.7) * 43758.5453123
  549.         return (v - math.floor(v)) * 2.0 - 1.0
  550.  
  551.     n00 = rnd(int(x0), int(y0))
  552.     n10 = rnd(int(x1), int(y0))
  553.     n01 = rnd(int(x0), int(y1))
  554.     n11 = rnd(int(x1), int(y1))
  555.  
  556.     nx0 = n00 * (1.0 - ux) + n10 * ux
  557.     nx1 = n01 * (1.0 - ux) + n11 * ux
  558.     return nx0 * (1.0 - uy) + nx1 * uy
  559.  
  560.  
  561. def fbm2d_hq(x: float, y: float) -> float:
  562.     # Multi-octave 2D fractal noise in [-1, 1].
  563.     total = 0.0
  564.     amp = 1.0
  565.     freq = 1.0
  566.     norm = 0.0
  567.     for _ in range(6):
  568.         total += noise2d_hq(x * freq, y * freq) * amp
  569.         norm += amp
  570.         amp *= 0.5
  571.         freq *= 2.0
  572.     return total / max(norm, 1e-6)
  573.  
  574.  
  575. def curl_noise(x: float, y: float, t: float) -> tuple[float, float]:
  576.     eps = 0.05
  577.     dn_dy = (scalar_noise(x, y + eps, t) - scalar_noise(x, y - eps, t)) / (2 * eps)
  578.     dn_dx = (scalar_noise(x + eps, y, t) - scalar_noise(x - eps, y, t)) / (2 * eps)
  579.     return dn_dy, -dn_dx
  580.  
  581.  
  582. def transport_surface(surface: pygame.Surface, sample_fn, fraction: float = 0.75, dim: float = 1.0, glow: float = 0.0) -> None:
  583.     src = surface.copy()
  584.     for y in range(GRID_SIZE):
  585.         for x in range(GRID_SIZE):
  586.             c0 = get_rgb(src, x, y)
  587.             sx, sy = sample_fn(x, y)
  588.             c1 = sample_rgb_bilinear(src, sx, sy)
  589.             nr = (c0[0] * (1.0 - fraction) + c1[0] * fraction) * dim
  590.             ng = (c0[1] * (1.0 - fraction) + c1[1] * fraction) * dim
  591.             nb = (c0[2] * (1.0 - fraction) + c1[2] * fraction) * dim
  592.             if glow > 0.0:
  593.                 vx = sx - x
  594.                 vy = sy - y
  595.                 g = clamp(math.hypot(vx, vy) * glow, 0.0, 1.0)
  596.                 nr = nr + g * 28.0
  597.                 ng = ng + g * 28.0
  598.                 nb = nb + g * 28.0
  599.                 peak = max(nr, ng, nb)
  600.                 if peak > 220.0:
  601.                     s = 220.0 / peak
  602.                     nr *= s
  603.                     ng *= s
  604.                     nb *= s
  605.             set_rgb(surface, x, y, (int(round(nr)), int(round(ng)), int(round(nb))))
  606.  
  607.  
  608. def apply_mode(surface: pygame.Surface, mode_idx: int, t: float, state: dict) -> None:
  609.     cx = (GRID_SIZE - 1) * 0.5
  610.     cy = (GRID_SIZE - 1) * 0.5
  611.  
  612.     if mode_idx == 0:
  613.         # No trail processing: frame is emitter-only (buffer clear happens in main loop).
  614.         return
  615.  
  616.     elif mode_idx == 1:
  617.         # Chromatic split transport: R/G/B sample from different vector fields.
  618.         src = surface.copy()
  619.         for y in range(GRID_SIZE):
  620.             for x in range(GRID_SIZE):
  621.                 dx = x - cx
  622.                 dy = y - cy
  623.                 r = math.hypot(dx, dy) + 1e-6
  624.                 th = math.atan2(dy, dx)
  625.                 # R inward spiral
  626.                 sxr = cx + math.cos(th - 0.18) * min(40.0, r + 0.35)
  627.                 syr = cy + math.sin(th - 0.18) * min(40.0, r + 0.35)
  628.                 # G outward
  629.                 sxg = cx + math.cos(th) * max(0.0, r - 0.35)
  630.                 syg = cy + math.sin(th) * max(0.0, r - 0.35)
  631.                 # B rotation
  632.                 sxb = cx + math.cos(th - 0.22) * r
  633.                 syb = cy + math.sin(th - 0.22) * r
  634.                 cr = sample_rgb_bilinear(src, sxr, syr)
  635.                 cg = sample_rgb_bilinear(src, sxg, syg)
  636.                 cb = sample_rgb_bilinear(src, sxb, syb)
  637.                 set_rgb(surface, x, y, (int(cr[0]), int(cg[1]), int(cb[2])))
  638.  
  639.     elif mode_idx == 2:
  640.         # Polar radial warp: periodic in/out scaling around grid center.
  641.         src = surface.copy()
  642.         pulse = 1.0 + 0.16 * math.sin(t * 2.3)
  643.         for y in range(GRID_SIZE):
  644.             for x in range(GRID_SIZE):
  645.                 dx = x - cx
  646.                 dy = y - cy
  647.                 r = math.hypot(dx, dy)
  648.                 th = math.atan2(dy, dx)
  649.                 sr = r / pulse
  650.                 sx = cx + math.cos(th) * sr
  651.                 sy = cy + math.sin(th) * sr
  652.                 c = sample_rgb_bilinear(src, sx, sy)
  653.                 set_rgb(surface, x, y, (int(c[0]), int(c[1]), int(c[2])))
  654.  
  655.     elif mode_idx == 3:
  656.         # Polar warp 2: outward-only projection by sampling from smaller radius.
  657.         src = surface.copy()
  658.         # Outward-only projection: sample from inner radius so content mirrors/flows outward.
  659.         outward_scale = 1.22
  660.         for y in range(GRID_SIZE):
  661.             for x in range(GRID_SIZE):
  662.                 dx = x - cx
  663.                 dy = y - cy
  664.                 r = math.hypot(dx, dy)
  665.                 th = math.atan2(dy, dx)
  666.                 sr = r / outward_scale
  667.                 sx = cx + math.cos(th) * sr
  668.                 sy = cy + math.sin(th) * sr
  669.                 c = sample_rgb_bilinear(src, sx, sy)
  670.                 set_rgb(surface, x, y, (int(c[0]), int(c[1]), int(c[2])))
  671.  
  672.     elif mode_idx == 4:
  673.         # Event-driven shockwave: radial displacement starts when center 2x2 gets newly lit.
  674.         trigger_time = state.get("shockwave_trigger_time", -1.0)
  675.         if trigger_time < 0.0:
  676.             return
  677.         elapsed = t - trigger_time
  678.         if elapsed < 0.0:
  679.             return
  680.         wave_speed = 18.0
  681.         wave_radius = elapsed * wave_speed
  682.         max_radius = math.hypot(cx, cy) + 1.5
  683.         if wave_radius > max_radius:
  684.             return
  685.         width = 1.3
  686.         def sample_fn(x, y):
  687.             dx = x - cx
  688.             dy = y - cy
  689.             r = math.hypot(dx, dy) + 1e-6
  690.             ux = dx / r
  691.             uy = dy / r
  692.             strength = 0.15
  693.             if abs(r - wave_radius) < width:
  694.                 strength = 1.2
  695.             return x - ux * strength, y - uy * strength
  696.         transport_surface(surface, sample_fn, fraction=0.82, dim=0.997)
  697.  
  698.     elif mode_idx == 5:
  699.         # Periodic shockwave: repeats every 500 ms independent of emitter-center trigger.
  700.         period = 0.5
  701.         phase = (t % period) / period
  702.         max_radius = math.hypot(cx, cy)
  703.         wave_radius = phase * max_radius
  704.         width = 1.1
  705.  
  706.         def sample_fn(x, y):
  707.             dx = x - cx
  708.             dy = y - cy
  709.             r = math.hypot(dx, dy) + 1e-6
  710.             ux = dx / r
  711.             uy = dy / r
  712.             strength = 0.0
  713.             if abs(r - wave_radius) < width:
  714.                 strength = 1.6
  715.             return x - ux * strength, y - uy * strength
  716.  
  717.         transport_surface(surface, sample_fn, fraction=0.9, dim=0.998)
  718.  
  719.     elif mode_idx == 6:
  720.         # Rotating coordinate sampling: global frame sampled in a sinusoidally rotated basis.
  721.         src = surface.copy()
  722.         angle = math.sin(t) * 0.3
  723.         ca = math.cos(angle)
  724.         sa = math.sin(angle)
  725.         for y in range(GRID_SIZE):
  726.             for x in range(GRID_SIZE):
  727.                 dx = x - cx
  728.                 dy = y - cy
  729.                 sx = cx + (dx * ca - dy * sa)
  730.                 sy = cy + (dx * sa + dy * ca)
  731.                 c = sample_rgb_bilinear(src, sx, sy)
  732.                 set_rgb(surface, x, y, (int(c[0]), int(c[1]), int(c[2])))
  733.  
  734.     elif mode_idx == 7:
  735.         # Rotating wind advection: global directional smear, wind vector rotates over time.
  736.         # Global directional smear with slowly rotating wind direction.
  737.         wind_angle = t * (2.0 * math.pi * 0.25)
  738.         wind_step = 0.95
  739.         wx = math.cos(wind_angle) * wind_step
  740.         wy = math.sin(wind_angle) * wind_step
  741.  
  742.         def sample_fn(x, y):
  743.             # Backward sampling along wind keeps advection stable.
  744.             return x - wx, y - wy
  745.  
  746.         transport_surface(surface, sample_fn, fraction=0.86, dim=0.999)
  747.  
  748.     elif mode_idx == 8:
  749.         # Rotating wind wave: linear advection + sinusoidal perpendicular wobble.
  750.         wind_angle = t * (2.0 * math.pi * 0.25)
  751.         wind_step = 0.95
  752.         wx = math.cos(wind_angle)
  753.         wy = math.sin(wind_angle)
  754.         # Unit perpendicular to current wind direction.
  755.         px = -wy
  756.         py = wx
  757.         wave_amp = 0.65
  758.         wave_freq = 0.20
  759.         wave_speed = 1.20
  760.  
  761.         def sample_fn(x, y):
  762.             # 1) Backward advection along linear wind.
  763.             sx = x - wx * wind_step
  764.             sy = y - wy * wind_step
  765.             # 2) Sinus wobble only along perpendicular axis.
  766.             proj = sx * wx + sy * wy
  767.             wobble = math.sin(proj * wave_freq + t * wave_speed) * wave_amp
  768.             sx += px * wobble
  769.             sy += py * wobble
  770.             return sx, sy
  771.  
  772.         transport_surface(surface, sample_fn, fraction=0.88, dim=0.999)
  773.  
  774.     elif mode_idx == 9:
  775.         # Meandering jet: dominant flow axis + sinusoidal transverse drift + slight turbulence.
  776.         # Main stream direction is gently steered by 1D noise over time.
  777.         dir_noise = scalar_noise(0.0, 0.0, t * 0.25)
  778.         # Map noise [-1, 1] to full [0, 2pi] heading range.
  779.         base_angle = (dir_noise + 1.0) * math.pi
  780.         bx = math.cos(base_angle)
  781.         by = math.sin(base_angle)
  782.         px = -by
  783.         py = bx
  784.         base_speed = 1.05
  785.         wave_freq = 0.34
  786.         wave_speed = 1.15
  787.         noise_freq = 0.28
  788.         noise_speed = 0.95
  789.         wave_amp = 0.72
  790.         turb_amp = 0.36
  791.         phase_drift = t * 0.42
  792.         transport_fraction = 0.90
  793.         transport_dim = 0.999
  794.         hue_drift_strength = 0.085
  795.         src = surface.copy()
  796.  
  797.         for y in range(GRID_SIZE):
  798.             for x in range(GRID_SIZE):
  799.                 # Position projected onto base flow axis controls meander phase.
  800.                 s = x * bx + y * by
  801.                 sin_meander = math.sin(s * wave_freq + phase_drift + t * wave_speed) * wave_amp
  802.                 noise_meander = scalar_noise(s * noise_freq + phase_drift, 0.0, t * noise_speed) * wave_amp
  803.                 meander = sin_meander + noise_meander
  804.                 # Small turbulence keeps the stream organic without destroying the main channel.
  805.                 turb = scalar_noise(x * 0.33, y * 0.33, t * 0.8) * turb_amp
  806.                 vx = bx * base_speed + px * (meander + turb)
  807.                 vy = by * base_speed + py * (meander + turb)
  808.  
  809.                 sx = x - vx
  810.                 sy = y - vy
  811.                 c0 = get_rgb(src, x, y)
  812.                 c1 = sample_rgb_bilinear(src, sx, sy)
  813.                 nr = (c0[0] * (1.0 - transport_fraction) + c1[0] * transport_fraction) * transport_dim
  814.                 ng = (c0[1] * (1.0 - transport_fraction) + c1[1] * transport_fraction) * transport_dim
  815.                 nb = (c0[2] * (1.0 - transport_fraction) + c1[2] * transport_fraction) * transport_dim
  816.  
  817.                 # Hue drift along local velocity angle.
  818.                 h, sv, vv = colorsys.rgb_to_hsv(
  819.                     clamp(nr / 255.0, 0.0, 1.0),
  820.                     clamp(ng / 255.0, 0.0, 1.0),
  821.                     clamp(nb / 255.0, 0.0, 1.0),
  822.                 )
  823.                 angle_norm = math.atan2(vy, vx) / (2.0 * math.pi)
  824.                 h = (h + angle_norm * hue_drift_strength) % 1.0
  825.                 rr, gg, bb = colorsys.hsv_to_rgb(h, sv, vv)
  826.                 set_rgb(surface, x, y, (int(rr * 255), int(gg * 255), int(bb * 255)))
  827.  
  828.     elif mode_idx == 10:
  829.         # Attractor field: inverse-square pull plus tangential swirl around moving attractors.
  830.         def sample_fn(x, y):
  831.             dx = dy = 0.0
  832.             at = [
  833.                 (cx + 7.0 * math.sin(t * 0.7), cy + 7.0 * math.cos(t * 0.9)),
  834.                 (cx + 6.0 * math.cos(t * 1.1), cy + 6.0 * math.sin(t * 0.8)),
  835.             ]
  836.             for ax, ay in at:
  837.                 vx = ax - x
  838.                 vy = ay - y
  839.                 d2 = vx * vx + vy * vy + 2.0
  840.                 inv = 1.0 / d2
  841.                 dx += vx * inv * 9.0
  842.                 dy += vy * inv * 9.0
  843.                 dx += -vy * inv * 4.0
  844.                 dy += vx * inv * 4.0
  845.             return x - dx, y - dy
  846.         transport_surface(surface, sample_fn, fraction=0.8, dim=0.997)
  847.  
  848.     elif mode_idx == 11:
  849.         # Fractal spiral advection: spiral angle depends on radius and time.
  850.         def sample_fn(x, y):
  851.             dx = x - cx
  852.             dy = y - cy
  853.             r = math.hypot(dx, dy)
  854.             th = math.atan2(dy, dx)
  855.             ang_step = 0.18 + 0.22 * math.sin(r * 0.45 + t * 0.8)
  856.             sr = min(40.0, r + 0.28)
  857.             sx = cx + math.cos(th - ang_step) * sr
  858.             sy = cy + math.sin(th - ang_step) * sr
  859.             return sx, sy
  860.         transport_surface(surface, sample_fn, fraction=0.82, dim=0.998)
  861.     elif mode_idx == 12:
  862.         # Ring Flow: soft-blended concentric zones (inner CW swirl, middle outward drift, outer CCW swirl).
  863.         src = surface.copy()
  864.         max_r = math.hypot(cx, cy)
  865.         for y in range(GRID_SIZE):
  866.             for x in range(GRID_SIZE):
  867.                 dx = x - cx
  868.                 dy = y - cy
  869.                 r = math.hypot(dx, dy)
  870.                 rn = r / max_r if max_r > 1e-6 else 0.0
  871.                 th = math.atan2(dy, dx)
  872.  
  873.                 # Smooth radial zone weights (gaussian-like) for seamless transitions.
  874.                 w_inner = math.exp(-((rn - 0.23) / 0.17) ** 2)
  875.                 w_mid = math.exp(-((rn - 0.56) / 0.18) ** 2)
  876.                 w_outer = math.exp(-((rn - 0.86) / 0.16) ** 2)
  877.                 w_sum = w_inner + w_mid + w_outer + 1e-6
  878.                 w_inner /= w_sum
  879.                 w_mid /= w_sum
  880.                 w_outer /= w_sum
  881.  
  882.                 ang = (-0.26 * w_inner) + (0.24 * w_outer)
  883.                 drift = 0.42 * w_mid
  884.  
  885.                 # Backward sampling for stable advection:
  886.                 # outward drift samples slightly inward radius.
  887.                 sample_r = clamp(r - drift, 0.0, max_r + 1.5)
  888.                 sample_th = th - ang
  889.                 sx = cx + math.cos(sample_th) * sample_r
  890.                 sy = cy + math.sin(sample_th) * sample_r
  891.  
  892.                 c = sample_rgb_bilinear(src, sx, sy)
  893.                 set_rgb(surface, x, y, (int(c[0]), int(c[1]), int(c[2])))
  894.  
  895.     elif mode_idx == 13:
  896.         # Square spiral stream: ring-by-ring orthogonal transport on square shells.
  897.         apply_square_spiral_tail(
  898.             surface,
  899.             GRID_SIZE // 2,
  900.             GRID_SIZE // 2,
  901.             (GRID_SIZE // 2) - 1,
  902.             SPIRAL_TRANSPORT_FRACTION,
  903.             SPIRAL_DIM_DEFAULT,
  904.         )
  905.     elif mode_idx == 14:
  906.         # Round spiral inward: backward sample from larger radius to pull content inward.
  907.         apply_round_spiral_tail(
  908.             surface,
  909.             cx,
  910.             cy,
  911.             math.hypot(cx, cy),
  912.             SPIRAL_TRANSPORT_FRACTION,
  913.             SPIRAL_DIM_DEFAULT,
  914.             outward=False,
  915.         )
  916.     elif mode_idx == 15:
  917.         # Round spiral outward: backward sample from smaller radius to push outward.
  918.         apply_round_spiral_tail(
  919.             surface,
  920.             cx,
  921.             cy,
  922.             math.hypot(cx, cy),
  923.             SPIRAL_TRANSPORT_FRACTION,
  924.             SPIRAL_DIM_DEFAULT,
  925.             outward=True,
  926.         )
  927.     elif mode_idx == 16:
  928.         # To center: radial inward transport toward center for all pixels.
  929.         apply_to_center_tail(
  930.             surface,
  931.             cx,
  932.             cy,
  933.             SPIRAL_TRANSPORT_FRACTION,
  934.             SPIRAL_DIM_DEFAULT,
  935.         )
  936.     elif mode_idx == 17:
  937.         # From center: radial outward transport from center for all pixels.
  938.         apply_from_center_tail(
  939.             surface,
  940.             cx,
  941.             cy,
  942.             SPIRAL_TRANSPORT_FRACTION,
  943.             SPIRAL_DIM_DEFAULT,
  944.         )
  945.     elif mode_idx == 18:
  946.         # Directional noise: rotating wind advection with radius-dependent widening of the tail.
  947.         wind_angle = t * (2.0 * math.pi * 0.25)
  948.         wx = math.cos(wind_angle)
  949.         wy = math.sin(wind_angle)
  950.         px = -wy
  951.         py = wx
  952.         wind_step = 0.95
  953.         max_r = math.hypot(cx, cy)
  954.  
  955.         def sample_fn(x, y):
  956.             dx = x - cx
  957.             dy = y - cy
  958.             r = math.hypot(dx, dy)
  959.             rn = clamp(r / max_r, 0.0, 1.0)
  960.             s = x * wx + y * wy
  961.             # Outward-shifting noise phase: fronts propagate away from emitter center.
  962.             radial_phase = (r - t * 10.0) * 0.38
  963.             n = noise1d(s * 0.12 + radial_phase)
  964.             # Add short-wavelength sinus modulation on top of noise modulation.
  965.             short_wave = math.sin(s * 1.45 + radial_phase * 2.8) * 0.48
  966.             n += short_wave
  967.             # Strong nonlinear widening toward the outer region.
  968.             width_gain = rn ** 2.2
  969.             lateral = n * (0.05 + 2.40 * width_gain)
  970.             sx = x - (wx * wind_step + px * lateral)
  971.             sy = y - (wy * wind_step + py * lateral)
  972.             return sx, sy
  973.  
  974.         transport_surface(surface, sample_fn, fraction=0.88, dim=0.999)
  975.     elif mode_idx == 19:
  976.         # Directional wind base transport; strong noise alpha mask is applied later (after fade).
  977.         wind_angle = t * (2.0 * math.pi * 0.25)
  978.         wx = math.cos(wind_angle)
  979.         wy = math.sin(wind_angle)
  980.         wind_step = 0.95
  981.         transport_fraction = 0.86
  982.         transport_dim = 0.999
  983.  
  984.         # Base directional wind (same behavior as rotating wind mode).
  985.         def sample_fn(x, y):
  986.             return x - wx * wind_step, y - wy * wind_step
  987.  
  988.         transport_surface(surface, sample_fn, fraction=transport_fraction, dim=transport_dim)
  989.  
  990.  
  991. def apply_mode19_noise_alpha_mask(surface: pygame.Surface, t: float, state: dict) -> None:
  992.     # Final pass: strong HQ 2D alpha mask after all other dimming.
  993.     wind_angle = t * (2.0 * math.pi * 0.25)
  994.     wx = math.cos(wind_angle)
  995.     wy = math.sin(wind_angle)
  996.     noise_scale = 0.09
  997.     noise_drift = 1.35
  998.     dt = float(state.get("dt", 1.0 / FPS))
  999.     state["noise_offset_x"] = float(state.get("noise_offset_x", 0.0)) + wx * noise_drift * dt
  1000.     state["noise_offset_y"] = float(state.get("noise_offset_y", 0.0)) + wy * noise_drift * dt
  1001.     off_x = state["noise_offset_x"]
  1002.     off_y = state["noise_offset_y"]
  1003.  
  1004.     src = surface.copy()
  1005.     for y in range(GRID_SIZE):
  1006.         for x in range(GRID_SIZE):
  1007.             nx = x * noise_scale - off_x
  1008.             ny = y * noise_scale - off_y
  1009.             n = fbm2d_hq(nx, ny)
  1010.             alpha = clamp((0.5 + 0.5 * n - 0.5) * 3.6 + 0.5, 0.0, 1.0)
  1011.             dim_mask_base = 0.06 + 0.94 * alpha
  1012.             dim_mask = 1.0 * 0.875 + dim_mask_base * 0.125
  1013.             c = get_rgb(src, x, y)
  1014.             set_rgb(
  1015.                 surface,
  1016.                 x,
  1017.                 y,
  1018.                 (
  1019.                     int(clamp(c[0] * dim_mask, 0.0, 255.0)),
  1020.                     int(clamp(c[1] * dim_mask, 0.0, 255.0)),
  1021.                     int(clamp(c[2] * dim_mask, 0.0, 255.0)),
  1022.                 ),
  1023.             )
  1024.  
  1025.  
  1026. def apply_global_fade(surface: pygame.Surface, fade_factor: float) -> None:
  1027.     for y in range(GRID_SIZE):
  1028.         for x in range(GRID_SIZE):
  1029.             c = surface.get_at((x, y))
  1030.             surface.set_at((x, y), (int(round(c.r * fade_factor)), int(round(c.g * fade_factor)), int(round(c.b * fade_factor))))
  1031.  
  1032.  
  1033. def apply_blur_2x2(surface: pygame.Surface) -> None:
  1034.     src = surface.copy()
  1035.     for y in range(GRID_SIZE):
  1036.         for x in range(GRID_SIZE):
  1037.             r_sum = 0.0
  1038.             g_sum = 0.0
  1039.             b_sum = 0.0
  1040.             for oy in (-1, 0, 1):
  1041.                 sy = clamp(y + oy, 0, GRID_SIZE - 1)
  1042.                 for ox in (-1, 0, 1):
  1043.                     sx = clamp(x + ox, 0, GRID_SIZE - 1)
  1044.                     c = get_rgb(src, int(sx), int(sy))
  1045.                     r_sum += c[0]
  1046.                     g_sum += c[1]
  1047.                     b_sum += c[2]
  1048.             r = r_sum / 9.0
  1049.             g = g_sum / 9.0
  1050.             b = b_sum / 9.0
  1051.             set_rgb(surface, x, y, (int(r), int(g), int(b)))
  1052.  
  1053.  
  1054. def center_4_energy(surface: pygame.Surface) -> float:
  1055.     c0 = (GRID_SIZE // 2) - 1
  1056.     c1 = GRID_SIZE // 2
  1057.     e = 0.0
  1058.     for y in (c0, c1):
  1059.         for x in (c0, c1):
  1060.             r, g, b = get_rgb(surface, x, y)
  1061.             e += r + g + b
  1062.     return e
  1063.  
  1064.  
  1065. def main() -> None:
  1066.     pygame.init()
  1067.     screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
  1068.     pygame.display.set_caption("Prototypes")
  1069.     clock = pygame.time.Clock()
  1070.  
  1071.     font = pygame.font.SysFont("arial", 16)
  1072.     font_small = pygame.font.SysFont("arial", 12)
  1073.  
  1074.     grid_rect = pygame.Rect(MARGIN, MARGIN, GRID_PIXELS, GRID_PIXELS)
  1075.     grid_surface = pygame.Surface((GRID_SIZE, GRID_SIZE))
  1076.     grid_surface.fill((0, 0, 0))
  1077.  
  1078.     ui_x = grid_rect.right + MARGIN
  1079.     panel_rect = pygame.Rect(ui_x, MARGIN, RIGHT_UI_WIDTH, WINDOW_HEIGHT - 2 * MARGIN)
  1080.  
  1081.     def slider_y(idx: int) -> int:
  1082.         return MARGIN + 128 + idx * 56
  1083.  
  1084.     sliders = [
  1085.         Slider("Endpoint Speed", 0.00, 2.00, ENDPOINT_SPEED_DEFAULT, pygame.Rect(ui_x + 20, slider_y(0), RIGHT_UI_WIDTH - 40, 14), 2),
  1086.         Slider("Color Cycling Speed", 0.00, 1.00, COLOR_SHIFT_DEFAULT, pygame.Rect(ui_x + 20, slider_y(1), RIGHT_UI_WIDTH - 40, 14), 2),
  1087.         Slider("Fade %", 95.0, 100.0, 99.995, pygame.Rect(ui_x + 20, slider_y(2), RIGHT_UI_WIDTH - 40, 14), 3),
  1088.     ]
  1089.  
  1090.     emitter_gap = 6
  1091.     emitter_btn_w = (RIGHT_UI_WIDTH - 40 - emitter_gap * 3) // 4
  1092.     emitter_line_button = Button("Line", pygame.Rect(ui_x + 20, MARGIN + 58, emitter_btn_w, 28), active=True)
  1093.     emitter_dots_button = Button("Dots", pygame.Rect(ui_x + 20 + emitter_btn_w + emitter_gap, MARGIN + 58, emitter_btn_w, 28), active=False)
  1094.     emitter_triangle_button = Button("Triangle", pygame.Rect(ui_x + 20 + (emitter_btn_w + emitter_gap) * 2, MARGIN + 58, emitter_btn_w, 28), active=False)
  1095.     emitter_ring_button = Button("Ring", pygame.Rect(ui_x + 20 + (emitter_btn_w + emitter_gap) * 3, MARGIN + 58, emitter_btn_w, 28), active=False)
  1096.  
  1097.     mode_buttons: list[Button] = []
  1098.     mode_grid_y = MARGIN + 260
  1099.     mode_gap_x = 8
  1100.     mode_gap_y = 4
  1101.     mode_cols = 2
  1102.     mode_btn_w = (RIGHT_UI_WIDTH - 40 - mode_gap_x) // mode_cols
  1103.     mode_btn_h = 18
  1104.     for idx, label in enumerate(MODE_BUTTON_LABELS):
  1105.         col = idx % mode_cols
  1106.         row = idx // mode_cols
  1107.         bx = ui_x + 20 + col * (mode_btn_w + mode_gap_x)
  1108.         by = mode_grid_y + row * (mode_btn_h + mode_gap_y)
  1109.         mode_buttons.append(Button(label, pygame.Rect(bx, by, mode_btn_w, mode_btn_h), active=(idx == 0)))
  1110.     mode_rows = (len(MODE_BUTTON_LABELS) + mode_cols - 1) // mode_cols
  1111.     blur_button = Button("Blur", pygame.Rect(ui_x + 20, mode_grid_y + mode_rows * (mode_btn_h + mode_gap_y) + 6, RIGHT_UI_WIDTH - 40, 24), active=False)
  1112.  
  1113.     state = {
  1114.         "history": [grid_surface.copy(), grid_surface.copy(), grid_surface.copy()],
  1115.         "rd": [[0.0 for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)],
  1116.         "frame": 0,
  1117.         "fade": 0.999,
  1118.         "shockwave_trigger_time": -1.0,
  1119.         "shockwave_center_latched": False,
  1120.         "prev_t": 0.0,
  1121.         "dt": 1.0 / FPS,
  1122.         "noise_offset_x": 0.0,
  1123.         "noise_offset_y": 0.0,
  1124.     }
  1125.  
  1126.     emitter_mode = "line"
  1127.     mode_idx = 0
  1128.     blur_enabled = False
  1129.  
  1130.     start_time = pygame.time.get_ticks() / 1000.0
  1131.     running = True
  1132.     while running:
  1133.         clock.tick(0)
  1134.  
  1135.         for event in pygame.event.get():
  1136.             if event.type == pygame.QUIT:
  1137.                 running = False
  1138.  
  1139.             if emitter_line_button.handle_event(event):
  1140.                 emitter_mode = "line"
  1141.                 emitter_line_button.active = True
  1142.                 emitter_dots_button.active = False
  1143.                 emitter_triangle_button.active = False
  1144.                 emitter_ring_button.active = False
  1145.             if emitter_dots_button.handle_event(event):
  1146.                 emitter_mode = "orbiting_dots"
  1147.                 emitter_line_button.active = False
  1148.                 emitter_dots_button.active = True
  1149.                 emitter_triangle_button.active = False
  1150.                 emitter_ring_button.active = False
  1151.             if emitter_triangle_button.handle_event(event):
  1152.                 emitter_mode = "triangle"
  1153.                 emitter_line_button.active = False
  1154.                 emitter_dots_button.active = False
  1155.                 emitter_triangle_button.active = True
  1156.                 emitter_ring_button.active = False
  1157.             if emitter_ring_button.handle_event(event):
  1158.                 emitter_mode = "ring"
  1159.                 emitter_line_button.active = False
  1160.                 emitter_dots_button.active = False
  1161.                 emitter_triangle_button.active = False
  1162.                 emitter_ring_button.active = True
  1163.  
  1164.             for i, btn in enumerate(mode_buttons):
  1165.                 if btn.handle_event(event):
  1166.                     mode_idx = i
  1167.                     for j, other in enumerate(mode_buttons):
  1168.                         other.active = (j == mode_idx)
  1169.             if blur_button.handle_event(event):
  1170.                 blur_enabled = not blur_enabled
  1171.                 blur_button.active = blur_enabled
  1172.  
  1173.             for slider in sliders:
  1174.                 slider.handle_event(event)
  1175.  
  1176.         t = pygame.time.get_ticks() / 1000.0 - start_time
  1177.         dt = t - float(state.get("prev_t", t))
  1178.         if dt < 0.0:
  1179.             dt = 0.0
  1180.         elif dt > 0.1:
  1181.             dt = 0.1
  1182.         state["dt"] = dt
  1183.         state["prev_t"] = t
  1184.         endpoint_speed, color_shift, fade_percent = [s.value for s in sliders]
  1185.         fade_factor = fade_percent / 100.0
  1186.         state["fade"] = fade_factor
  1187.  
  1188.         center_before = center_4_energy(grid_surface)
  1189.         if mode_idx == 0:
  1190.             grid_surface.fill((0, 0, 0))
  1191.             center_before = 0.0
  1192.  
  1193.         if emitter_mode == "line":
  1194.             inject_lissajous_line(grid_surface, t, color_shift, endpoint_speed)
  1195.         elif emitter_mode == "orbiting_dots":
  1196.             inject_orbiting_dots(grid_surface, t, endpoint_speed, color_shift)
  1197.         elif emitter_mode == "triangle":
  1198.             inject_lissajous_triangle(grid_surface, t, color_shift, endpoint_speed)
  1199.         else:
  1200.             inject_bouncing_ring(grid_surface, t, color_shift, endpoint_speed)
  1201.  
  1202.         if mode_idx == 4:
  1203.             center_after = center_4_energy(grid_surface)
  1204.             center_newly_drawn = (center_after - center_before) > 24.0
  1205.             if center_newly_drawn and not state["shockwave_center_latched"]:
  1206.                 state["shockwave_trigger_time"] = t
  1207.                 state["shockwave_center_latched"] = True
  1208.             elif not center_newly_drawn:
  1209.                 state["shockwave_center_latched"] = False
  1210.  
  1211.         if mode_idx != 0:
  1212.             apply_mode(grid_surface, mode_idx, t, state)
  1213.             apply_global_fade(grid_surface, fade_factor)
  1214.             if mode_idx == 19:
  1215.                 apply_mode19_noise_alpha_mask(grid_surface, t, state)
  1216.         if blur_enabled:
  1217.             apply_blur_2x2(grid_surface)
  1218.  
  1219.         # Update temporal history for echo-based modes.
  1220.         state["history"][2] = state["history"][1]
  1221.         state["history"][1] = state["history"][0]
  1222.         state["history"][0] = grid_surface.copy()
  1223.         state["frame"] += 1
  1224.  
  1225.         screen.fill(BG)
  1226.         scaled_grid = pygame.transform.scale(grid_surface, (GRID_PIXELS, GRID_PIXELS))
  1227.         screen.blit(scaled_grid, grid_rect.topleft)
  1228.  
  1229.         pygame.draw.rect(screen, PANEL, panel_rect, border_radius=10)
  1230.         title = font.render("Prototypes", True, TEXT)
  1231.         screen.blit(title, (panel_rect.left + 20, panel_rect.top + 20))
  1232.         fps_text = font.render(f"FPS: {clock.get_fps():5.1f}", True, TEXT)
  1233.         screen.blit(fps_text, (panel_rect.right - fps_text.get_width() - 20, panel_rect.top + 20))
  1234.  
  1235.         emitter_line_button.draw(screen, font)
  1236.         emitter_dots_button.draw(screen, font)
  1237.         emitter_triangle_button.draw(screen, font)
  1238.         emitter_ring_button.draw(screen, font)
  1239.  
  1240.         mode_header = font_small.render("Trail Modes", True, TEXT)
  1241.         screen.blit(mode_header, (panel_rect.left + 20, mode_grid_y - 18))
  1242.         for btn in mode_buttons:
  1243.             btn.draw(screen, font_small)
  1244.         blur_button.draw(screen, font)
  1245.  
  1246.         for slider in sliders:
  1247.             slider.draw(screen, font)
  1248.  
  1249.         pygame.display.flip()
  1250.  
  1251.     pygame.quit()
  1252.     sys.exit(0)
  1253.  
  1254.  
  1255. if __name__ == "__main__":
  1256.     main()
  1257.  
Advertisement
Add Comment
Please, Sign In to add comment