grnMatrix.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. #!/usr/bin/env python3
  2. # TERMINAL_MODE
  3. import curses
  4. import random
  5. import time
  6. import math
  7. # Переменные для настройки
  8. SYMBOL_SET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*+-=[]{}|;:,.<>?/~│_─▔╴╵╶╷╌╎⌜⌝⌞⌟┌┐┘└╭╮╯╰┴┬⊺├┤┼⌈⌉⌊⌋"
  9. HEAD_COLOR = "white" # Цвет головы
  10. TAIL_GRADIENT_COLORS = ["cyan", "magenta", "magenta"]
  11. # Список цветов для градиента хвоста: "green" — зелёный, "red" — красный, "blue" — синий, "yellow" — жёлтый, "white" — белый, "cyan" — голубой, "magenta" — пурпурный
  12. SYMBOL_FALL_SPEED = 0.125 # Базовая скорость падения символов (строки за кадр)
  13. FADE_SPEED = 0.125 # Скорость затухания символов
  14. FRAME_RATE = 20 # Частота обновления экрана (кадров в секунду)
  15. TAIL_LENGTH_MIN = 5 # Минимальная длина хвоста колонки
  16. TAIL_LENGTH_MAX = 34 # Максимальная длина хвоста колонки
  17. FADE_STEPS = 256 # Максимальное количество шагов затухания символов
  18. BOTTOM_FADE_BOOST = 1.0 # Коэффициент ускорения затухания внизу экрана
  19. CHAR_CHANGE_RATE = 0.05 # Вероятность смены символа в хвосте
  20. COLUMN_DENSITY = 0.6 # Доля ширины экрана, заполненная колонками
  21. SYMBOL_BRIGHTNESS = 0.5 # Яркость символов (влияет на A_BOLD, A_NORMAL, A_DIM)
  22. TAIL_FADE_CURVE = 0.7 # Кривая затухания хвоста
  23. RANDOM_RESET_CHANCE = 0.002 # Вероятность случайного сброса колонки
  24. PAUSE_DURATION = 0.0 # Время паузы колонок после сброса
  25. BACKGROUND_CHAR = ' ' # Символ фона
  26. SYMBOL_DIVERSITY = 1.0 # Доля символов из SYMBOL_SET
  27. MOVEMENT_VARIATION = 0.05 # Диапазон отклонения скорости
  28. PULSE_RATE = 0.3 # Частота пульсации яркости
  29. SCREEN_FILL_RATE = 0.0 # Доля экрана, заполненная при старте
  30. PROGRESS_FILLED_CHAR = '│' # Символ для головы колонок
  31. TEXT_PULSE_AMPLITUDE = 0.0 # Амплитуда пульсации яркости
  32. EMOTION_INTENSITY = 0.6 # Интенсивность анимации
  33. CHARS = list(SYMBOL_SET)[:int(len(SYMBOL_SET) * SYMBOL_DIVERSITY)]
  34. class MatrixColumn:
  35. def __init__(self, max_y):
  36. self.max_y = max_y
  37. self.pos = random.randint(0, max_y - 1)
  38. self.speed = SYMBOL_FALL_SPEED + random.uniform(-MOVEMENT_VARIATION, MOVEMENT_VARIATION)
  39. self.tail_length = random.randint(TAIL_LENGTH_MIN, TAIL_LENGTH_MAX)
  40. self.pos_float = float(self.pos)
  41. self.tail_chars = [random.choice(CHARS) for _ in range(self.tail_length)]
  42. self.paused = PAUSE_DURATION > 0
  43. self.pause_time = time.time() if self.paused else 0
  44. def update(self, x, buffer, dirty_positions, color_pairs):
  45. if self.paused:
  46. if time.time() - self.pause_time >= PAUSE_DURATION:
  47. self.paused = False
  48. return
  49. self.pos_float += self.speed * EMOTION_INTENSITY
  50. new_pos = int(self.pos_float)
  51. if new_pos > self.max_y + self.tail_length or random.random() < RANDOM_RESET_CHANCE:
  52. for offset in range(self.tail_length):
  53. y = int(self.pos - offset)
  54. if 0 <= y < self.max_y and buffer[y][x][0] != BACKGROUND_CHAR:
  55. buffer[y][x] = (BACKGROUND_CHAR, curses.color_pair(0), 0.0)
  56. dirty_positions.add((y, x))
  57. self.pos = 0
  58. self.pos_float = 0.0
  59. self.tail_length = random.randint(TAIL_LENGTH_MIN, TAIL_LENGTH_MAX)
  60. self.tail_chars = [random.choice(CHARS) for _ in range(self.tail_length)]
  61. self.paused = PAUSE_DURATION > 0
  62. self.pause_time = time.time() if self.paused else 0
  63. else:
  64. old_tail_y = int(self.pos - self.tail_length)
  65. new_tail_y = new_pos - self.tail_length
  66. if 0 <= old_tail_y < self.max_y and old_tail_y < new_tail_y and buffer[old_tail_y][x][0] != BACKGROUND_CHAR:
  67. buffer[old_tail_y][x] = (BACKGROUND_CHAR, curses.color_pair(0), 0.0)
  68. dirty_positions.add((old_tail_y, x))
  69. self.pos = new_pos
  70. for i in range(self.tail_length):
  71. if random.random() < CHAR_CHANGE_RATE:
  72. self.tail_chars[i] = random.choice(CHARS)
  73. for offset in range(self.tail_length):
  74. y = self.pos - offset
  75. if 0 <= y < self.max_y:
  76. char = self.tail_chars[offset] if offset > 0 else PROGRESS_FILLED_CHAR
  77. fade_level = FADE_STEPS * (1 - (offset / self.tail_length) ** TAIL_FADE_CURVE)
  78. # Градиент цвета для хвоста
  79. if offset == 0:
  80. attr = curses.A_BOLD | curses.color_pair(1) # Голова
  81. else:
  82. gradient_step = min(offset / (self.tail_length - 1), 1.0) if self.tail_length > 1 else 0
  83. color_index = int(gradient_step * (len(color_pairs) - 2)) + 2 # Пропускаем пару 0 и 1
  84. attr = curses.A_NORMAL | curses.color_pair(color_index)
  85. if SYMBOL_BRIGHTNESS < 0.5 and offset > self.tail_length // 2:
  86. attr = curses.A_DIM | curses.color_pair(color_index)
  87. if buffer[y][x] != (char, attr, fade_level):
  88. buffer[y][x] = (char, attr, fade_level)
  89. dirty_positions.add((y, x))
  90. def main(stdscr):
  91. curses.curs_set(0)
  92. stdscr.timeout(0)
  93. # Инициализация цветов
  94. curses.start_color()
  95. curses.use_default_colors()
  96. color_map = {
  97. "green": curses.COLOR_GREEN, "red": curses.COLOR_RED, "blue": curses.COLOR_BLUE,
  98. "yellow": curses.COLOR_YELLOW, "white": curses.COLOR_WHITE, "cyan": curses.COLOR_CYAN,
  99. "magenta": curses.COLOR_MAGENTA
  100. }
  101. head_color = color_map.get(HEAD_COLOR.lower(), curses.COLOR_WHITE)
  102. curses.init_pair(1, head_color, -1) # Цвет головы
  103. # Инициализация градиента цветов для хвоста
  104. tail_colors = [color_map.get(c.lower(), curses.COLOR_YELLOW) for c in TAIL_GRADIENT_COLORS]
  105. color_pairs = [1] # Начальная пара для головы
  106. for i, color in enumerate(tail_colors, start=2):
  107. curses.init_pair(i, color, -1)
  108. color_pairs.append(i)
  109. stdscr.bkgd(BACKGROUND_CHAR, curses.color_pair(0))
  110. max_y, max_x = stdscr.getmaxyx()
  111. num_columns = int(max_x * COLUMN_DENSITY)
  112. columns = [MatrixColumn(max_y) for _ in range(num_columns)]
  113. buffer = [[(BACKGROUND_CHAR, curses.color_pair(0), 0.0) for _ in range(max_x)] for _ in range(max_y)]
  114. dirty_positions = set()
  115. if SCREEN_FILL_RATE > 0:
  116. for y in range(max_y):
  117. for x in range(max_x):
  118. if random.random() < SCREEN_FILL_RATE:
  119. stdscr.addch(y, x, random.choice(CHARS), curses.A_DIM | curses.color_pair(2))
  120. stdscr.refresh()
  121. time.sleep(0.01)
  122. pulse_time = time.time()
  123. while True:
  124. current_max_y, current_max_x = stdscr.getmaxyx()
  125. if current_max_y != max_y or current_max_x != max_x:
  126. max_y, max_x = current_max_y, current_max_x
  127. num_columns = int(max_x * COLUMN_DENSITY)
  128. columns = [MatrixColumn(max_y) for _ in range(num_columns)]
  129. buffer = [[(BACKGROUND_CHAR, curses.color_pair(0), 0.0) for _ in range(max_x)] for _ in range(max_y)]
  130. stdscr.clear()
  131. dirty_positions.clear()
  132. pulse_factor = 1.0
  133. if PULSE_RATE > 0:
  134. pulse_factor = 1.0 + TEXT_PULSE_AMPLITUDE * math.sin(2 * math.pi * PULSE_RATE * (time.time() - pulse_time))
  135. for y in range(max_y):
  136. for x in range(max_x):
  137. char, attr, fade_level = buffer[y][x]
  138. if fade_level > 0:
  139. fade_factor = FADE_SPEED * (1 + BOTTOM_FADE_BOOST * (y / max_y)) * pulse_factor
  140. new_fade_level = max(fade_level - fade_factor * EMOTION_INTENSITY, 0)
  141. if new_fade_level != fade_level:
  142. if new_fade_level == 0:
  143. buffer[y][x] = (BACKGROUND_CHAR, curses.color_pair(0), 0.0)
  144. else:
  145. buffer[y][x] = (char, attr, new_fade_level)
  146. dirty_positions.add((y, x))
  147. for i, col in enumerate(columns):
  148. col.max_y = max_y
  149. x = i * (max_x - 1) // (num_columns - 1) if num_columns > 1 else 0
  150. col.update(x, buffer, dirty_positions, color_pairs)
  151. for y, x in dirty_positions:
  152. char, attr, fade_level = buffer[y][x]
  153. try:
  154. if fade_level > FADE_STEPS * 0.8:
  155. stdscr.addch(y, x, char, curses.A_BOLD | attr)
  156. elif fade_level > FADE_STEPS * 0.6:
  157. stdscr.addch(y, x, char, curses.A_NORMAL | attr)
  158. else:
  159. stdscr.addch(y, x, char, curses.A_DIM | attr)
  160. except curses.error:
  161. pass
  162. dirty_positions.clear()
  163. stdscr.refresh()
  164. key = stdscr.getch()
  165. if key in (ord('q'), ord('Q')):
  166. break
  167. time.sleep(1 / FRAME_RATE)
  168. if __name__ == "__main__":
  169. curses.wrapper(main)