larry77

first formulation

May 14th, 2026
47
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 6.95 KB | None | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. ###############################################################################
  4. # Generative animal-like animation in Python
  5. #
  6. # Optimised MP4 version of the compact JavaScript / p5.js formula:
  7. #
  8. # a=(m,d=mag(k=9*cos(i/81),e=i/765-13)/4)=>point(
  9. # (q=79-2*sin(k*3)+
  10. # sin(k*k<19?t*3+d*4:d/2+4)/2*k*
  11. # (9+5*sin(d*d-e/6-t+m))
  12. # )*sin(c=d*d/9-t/16+m)+200,
  13. # (q+50)*cos(c)+200
  14. # )
  15. #
  16. # t=0,draw=$=>{
  17. # t||createCanvas(w=400,w);
  18. # background(9).stroke(w,96);
  19. # for(t+=PI/45,i=2e4;i--;)a(i%2*9)
  20. # }
  21. #
  22. # Main optimisation:
  23. #
  24. # - save MP4 directly with FFMpegWriter instead of GIF with PillowWriter
  25. # - precompute immutable arrays
  26. # - update one existing scatter object frame by frame
  27. ###############################################################################
  28.  
  29. import numpy as np
  30. import matplotlib.pyplot as plt
  31. from matplotlib.animation import FuncAnimation, FFMpegWriter
  32.  
  33. # =============================================================================
  34. # 1. Tunable parameters
  35. # =============================================================================
  36.  
  37. n_points = 20_000
  38.  
  39. # 540 * 4 frames at 25 fps gives:
  40. #
  41. # 2160 / 25 = 86.4 seconds
  42. n_frames = 540
  43.  
  44. fps = 25
  45.  
  46. canvas_width = 400
  47. canvas_height = 400
  48.  
  49. # Rendered output size.
  50. #
  51. # Final pixel size is:
  52. #
  53. # figsize_inches * dpi
  54. #
  55. # Since figsize is computed as pixels / dpi below, this gives 800 x 800.
  56. render_width_px = 600
  57. render_height_px = 600
  58. dpi = 150
  59.  
  60. # Visual tuning.
  61. #
  62. # Matplotlib scatter "s" is marker area in points squared.
  63. point_size = 1.5
  64. point_alpha = 0.45
  65.  
  66. background_colour = "#090909"
  67.  
  68. output_file = "living_animal_second_formula_python_thicker.mp4"
  69.  
  70. # FFmpeg encoding settings.
  71. #
  72. # crf:
  73. # lower = better quality / larger file
  74. # 18 is visually high quality
  75. #
  76. # preset:
  77. # faster presets encode faster but may produce larger files
  78. ffmpeg_crf = 18
  79. ffmpeg_preset = "veryfast"
  80. ffmpeg_threads = 4
  81.  
  82. # =============================================================================
  83. # 2. Static point data
  84. # =============================================================================
  85.  
  86. i = np.arange(n_points)
  87.  
  88. # JavaScript:
  89. #
  90. # k = 9 * cos(i / 81)
  91. k = 9 * np.cos(i / 81)
  92.  
  93. # JavaScript:
  94. #
  95. # e = i / 765 - 13
  96. e = i / 765 - 13
  97.  
  98. # JavaScript:
  99. #
  100. # d = mag(k, e) / 4
  101. #
  102. # In p5.js:
  103. #
  104. # mag(k, e) = sqrt(k^2 + e^2)
  105. d = np.sqrt(k**2 + e**2) / 4
  106.  
  107. # JavaScript:
  108. #
  109. # m = i % 2 * 9
  110. m = (i % 2) * 9
  111.  
  112. # =============================================================================
  113. # 3. Precomputed static terms
  114. # =============================================================================
  115.  
  116. # These quantities do not change with frame/time.
  117. # Precomputing them avoids repeated work inside compute_frame().
  118. k2 = k**2
  119. d2 = d**2
  120.  
  121. phase_mask = k2 < 19
  122. phase_else = d / 2 + 4
  123. d_times_4 = d * 4
  124.  
  125. q_static = 79 - 2 * np.sin(k * 3)
  126. q_multiplier = k / 2
  127.  
  128. wave_static = d2 - e / 6 + m
  129. c_static = d2 / 9 + m
  130.  
  131. # Preallocated coordinate array for matplotlib scatter offsets.
  132. #
  133. # Matplotlib expects an N x 2 array:
  134. #
  135. # [[x1, y1],
  136. # [x2, y2],
  137. # ...]
  138. offsets = np.empty((n_points, 2), dtype=np.float64)
  139.  
  140. # Preallocated phase array.
  141. phase_switch = np.empty(n_points, dtype=np.float64)
  142.  
  143. # =============================================================================
  144. # 4. Frame computation
  145. # =============================================================================
  146.  
  147. def compute_frame(frame_id):
  148. """
  149. Compute one frame of the animation.
  150.  
  151. Parameters
  152. ----------
  153. frame_id : int
  154. Integer frame number: 1, 2, 3, ...
  155.  
  156. The frame number is converted to continuous time as:
  157.  
  158. t = frame_id * pi / 45
  159.  
  160. matching the original JavaScript update:
  161.  
  162. t += PI / 45
  163.  
  164. Returns
  165. -------
  166. offsets : numpy.ndarray
  167. N x 2 array of x/y coordinates.
  168. """
  169.  
  170. t = frame_id * np.pi / 45
  171.  
  172. # JavaScript ternary:
  173. #
  174. # k*k < 19 ? t*3 + d*4 : d/2 + 4
  175. #
  176. # Instead of np.where(), fill a preallocated array.
  177. phase_switch[:] = phase_else
  178. phase_switch[phase_mask] = t * 3 + d_times_4[phase_mask]
  179.  
  180. # Main deformation term:
  181. #
  182. # q = 79 - 2*sin(k*3) +
  183. # sin(phase_switch)/2 * k *
  184. # (9 + 5*sin(d*d - e/6 - t + m))
  185. q = (
  186. q_static
  187. + np.sin(phase_switch)
  188. * q_multiplier
  189. * (9 + 5 * np.sin(wave_static - t))
  190. )
  191.  
  192. # Angular term:
  193. #
  194. # c = d*d/9 - t/16 + m
  195. c = c_static - t / 16
  196.  
  197. # Final coordinates:
  198. #
  199. # x = q * sin(c) + 200
  200. # y = (q + 50) * cos(c) + 200
  201. offsets[:, 0] = q * np.sin(c) + 200
  202. offsets[:, 1] = (q + 50) * np.cos(c) + 200
  203.  
  204. return offsets
  205.  
  206. # =============================================================================
  207. # 5. Matplotlib figure setup
  208. # =============================================================================
  209.  
  210. fig_width_inches = render_width_px / dpi
  211. fig_height_inches = render_height_px / dpi
  212.  
  213. fig, ax = plt.subplots(
  214. figsize=(fig_width_inches, fig_height_inches),
  215. dpi=dpi
  216. )
  217.  
  218. fig.patch.set_facecolor(background_colour)
  219. ax.set_facecolor(background_colour)
  220.  
  221. ax.set_xlim(0, canvas_width)
  222. ax.set_ylim(0, canvas_height)
  223. ax.set_aspect("equal")
  224.  
  225. ax.set_xticks([])
  226. ax.set_yticks([])
  227.  
  228. for spine in ax.spines.values():
  229. spine.set_visible(False)
  230.  
  231. ax.margins(0)
  232. plt.subplots_adjust(left=0, right=1, top=1, bottom=0)
  233.  
  234. # =============================================================================
  235. # 6. Initial frame and scatter object
  236. # =============================================================================
  237.  
  238. initial_offsets = compute_frame(1).copy()
  239.  
  240. points = ax.scatter(
  241. initial_offsets[:, 0],
  242. initial_offsets[:, 1],
  243. s=point_size,
  244. c="white",
  245. alpha=point_alpha,
  246. linewidths=0
  247. )
  248.  
  249. # =============================================================================
  250. # 7. Animation update function
  251. # =============================================================================
  252.  
  253. def update(frame_id):
  254. current_offsets = compute_frame(frame_id)
  255.  
  256. # Update the existing scatter object.
  257. # This avoids rebuilding the plot.
  258. points.set_offsets(current_offsets)
  259.  
  260. return (points,)
  261.  
  262. # =============================================================================
  263. # 8. Build and save animation directly as MP4
  264. # =============================================================================
  265.  
  266. animation = FuncAnimation(
  267. fig,
  268. update,
  269. frames=np.arange(1, n_frames + 1),
  270. interval=1000 / fps,
  271. blit=True
  272. )
  273.  
  274. writer = FFMpegWriter(
  275. fps=fps,
  276. codec="libx264",
  277. bitrate=-1,
  278. extra_args=[
  279. "-preset", ffmpeg_preset,
  280. "-crf", str(ffmpeg_crf),
  281. "-threads", str(ffmpeg_threads),
  282. "-pix_fmt", "yuv420p",
  283. ],
  284. )
  285.  
  286. animation.save(
  287. output_file,
  288. writer=writer
  289. )
  290.  
  291. plt.close(fig)
  292.  
  293. print(f"Saved: {output_file}")
  294.  
Advertisement
Add Comment
Please, Sign In to add comment