larry77

Untitled

May 13th, 2026 (edited)
168
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
R 10.71 KB | Source Code | 0 0
  1. ###############################################################################
  2. # Living Animal Animation in R
  3. #
  4. # Translation/adaptation of the Wolfram Mathematica animation from:
  5. # https://community.wolfram.com/groups/-/m/t/3713047
  6. #
  7. # This script:
  8. #   1. Creates a cloud of 10,000 points
  9. #   2. Applies nonlinear trigonometric transformations
  10. #   3. Recomputes the point positions over time
  11. #   4. Separates points into base/body points and highlighted tip points
  12. #   5. Changes the colour of the highlighted tips globally over time
  13. #   6. Exports the result as an animated GIF
  14. #
  15. # Main R packages:
  16. #   - ggplot2   : plotting
  17. #   - gganimate : animation
  18. #   - gifski    : GIF rendering backend
  19. #   - dplyr     : readable data manipulation
  20. #   - purrr     : readable iteration over frames
  21. ###############################################################################
  22.  
  23. # -----------------------------------------------------------------------------
  24. # Load required packages
  25. # -----------------------------------------------------------------------------
  26.  
  27. library(ggplot2)
  28. library(gganimate)
  29. library(gifski)
  30. library(dplyr)
  31. library(purrr)
  32.  
  33. # -----------------------------------------------------------------------------
  34. # Animation parameters
  35. #
  36. # n_frames:
  37. #   Number of frames in the animation.
  38. #
  39. # fps:
  40. #   Frames per second.
  41. #
  42. # Higher fps makes motion smoother but sometimes less organic.
  43. # You found that fps = 25 looks better than fps = 30.
  44. #
  45. # duration in seconds is approximately:
  46. #
  47. #   n_frames / fps
  48. #
  49. # With 180 frames and 25 fps:
  50. #
  51. #   180 / 25 = 7.2 seconds
  52. # -----------------------------------------------------------------------------
  53.  
  54. n_frames <- 180
  55. fps <- 25
  56.  
  57. output_file <- "living_animal.gif"
  58.  
  59. # -----------------------------------------------------------------------------
  60. # Generate the base point index
  61. #
  62. # Mathematica equivalent:
  63. #
  64. #   x = N @ Range[0, 9999]
  65. #
  66. # These are not yet the final x-coordinates.
  67. # They are an index used to generate the point cloud.
  68. # -----------------------------------------------------------------------------
  69.  
  70. x <- seq(0, 9999)
  71.  
  72. # -----------------------------------------------------------------------------
  73. # Precompute quantities that do not depend on time
  74. #
  75. # These values are static, so we compute them once and reuse them for every frame.
  76. #
  77. # Mathematica logic:
  78. #
  79. #   y = x / 235
  80. #   k = 4 Cos[x / 21]
  81. #   e = y / 8 - 20
  82. #   d = Sqrt[k^2 + e^2]
  83. #
  84. # Meaning of the variables:
  85. #
  86. #   y_index:
  87. #     auxiliary scaled index
  88. #
  89. #   k:
  90. #     oscillatory component
  91. #
  92. #   e:
  93. #     slow linear component
  94. #
  95. #   d:
  96. #     distance-like quantity combining k and e
  97. #
  98. #   group:
  99. #     separates highlighted "tip" points from the rest of the body
  100. # -----------------------------------------------------------------------------
  101.  
  102. base_data <- tibble(
  103.   x_index = x,
  104.  
  105.   # Auxiliary variable used in the original formula
  106.   y_index = x / 235,
  107.  
  108.   # Oscillatory component
  109.   k = 4 * cos(x / 21)
  110. ) |>
  111.   mutate(
  112.     # Slow linear component
  113.     e = y_index / 8 - 20,
  114.  
  115.     # Distance-like quantity
  116.     d = sqrt(k^2 + e^2),
  117.  
  118.     # Equivalent to the Mathematica selection of highlighted points:
  119.     #
  120.     #   k^2 > 15
  121.     #
  122.     # These points are drawn larger and with a colour that changes over time.
  123.     group = if_else(k^2 > 15, "tips", "base")
  124.   )
  125.  
  126. # -----------------------------------------------------------------------------
  127. # Function generating one animation frame
  128. #
  129. # Inputs:
  130. #
  131. #   t:
  132. #     current animation time
  133. #
  134. #   frame_id:
  135. #     integer frame number used by gganimate
  136. #
  137. #   data:
  138. #     the static base data created above
  139. #
  140. # Output:
  141. #
  142. #   A tibble containing final x/y coordinates, group, colour, and frame number.
  143. #
  144. # The main time-dependent formula is q.
  145. # It controls much of the organic deformation of the shape.
  146. # -----------------------------------------------------------------------------
  147.  
  148. make_frame <- function(t, frame_id, data = base_data) {
  149.  
  150.   # ---------------------------------------------------------------------------
  151.   # Global colour oscillation
  152.   #
  153.   # Original Mathematica colour expression:
  154.   #
  155.   #   Blend[{White, Red}, Sin[t]^2]
  156.   #
  157.   # This means:
  158.   #
  159.   #   sin(t)^2 = 0  -> white
  160.   #   sin(t)^2 = 1  -> red
  161.   #
  162.   # In RGB terms:
  163.   #
  164.   #   white = rgb(1, 1, 1)
  165.   #   red   = rgb(1, 0, 0)
  166.   #
  167.   # So the blend can be represented as:
  168.   #
  169.   #   rgb(1, 1 - intensity, 1 - intensity)
  170.   #
  171.   # where:
  172.   #
  173.   #   intensity = sin(t)^2
  174.   # ---------------------------------------------------------------------------
  175.  
  176.   red_intensity <- sin(t)^2
  177.  
  178.   tips_colour <- rgb(
  179.     red = 1,
  180.     green = 1 - red_intensity,
  181.     blue = 1 - red_intensity
  182.   )
  183.  
  184.   # ---------------------------------------------------------------------------
  185.   # Compute the frame-specific coordinates
  186.   # ---------------------------------------------------------------------------
  187.  
  188.   data |>
  189.     mutate(
  190.       # Equivalent to Mathematica:
  191.       #
  192.       #   c = d - t
  193.       #
  194.       # This creates a rotating phase shift over time.
  195.       c = d - t,
  196.  
  197.       # Main nonlinear deformation term.
  198.       #
  199.       # Mathematica equivalent:
  200.       #
  201.       #   q = 3 Sin[2 k] + 0.3/k +
  202.       #       Sin[y/19] k (9 + 2 Sin[14 e - 3 d + 2 t])
  203.       #
  204.       # This combines:
  205.       #
  206.       #   - local oscillations through sin(2 * k)
  207.       #   - a small inverse-k term
  208.       #   - a larger time-varying deformation
  209.       q =
  210.         3 * sin(2 * k) +
  211.         0.3 / k +
  212.         sin(y_index / 19) * k *
  213.         (9 + 2 * sin(14 * e - 3 * d + 2 * t)),
  214.  
  215.       # Final x coordinate.
  216.       #
  217.       # Mathematica equivalent:
  218.       #
  219.       #   q + 50 Cos[c] + 200
  220.       #
  221.       # The +200 recentres the image horizontally.
  222.       x = q + 50 * cos(c) + 200,
  223.  
  224.       # Final y coordinate.
  225.       #
  226.       # Mathematica equivalent:
  227.       #
  228.       #   400 - (q Sin[c] + 39 d - 475)
  229.       #
  230.       # This simplifies to:
  231.       #
  232.       #   875 - q Sin[c] - 39 d
  233.       #
  234.       # The expression also flips/repositions the vertical direction.
  235.       y = 875 - q * sin(c) - 39 * d,
  236.  
  237.       # Frame identifier used by gganimate.
  238.       frame = frame_id,
  239.  
  240.       # Colour for each point.
  241.       #
  242.       # Base/body points remain white.
  243.       # Tip points use the globally changing colour.
  244.       colour = if_else(group == "tips", tips_colour, "white")
  245.     ) |>
  246.     select(x, y, group, colour, frame)
  247. }
  248.  
  249. # -----------------------------------------------------------------------------
  250. # Define the animation timeline
  251. #
  252. # The original Mathematica animation is usually parameterised over 0 to 2*pi.
  253. #
  254. # Here we use 0 to 6*pi, which gives a longer animation and makes the movement
  255. # easier to appreciate.
  256. #
  257. # To make it closer to the original, use:
  258. #
  259. #   times <- seq(0, 2 * pi, length.out = n_frames)
  260. # -----------------------------------------------------------------------------
  261.  
  262. times <- seq(
  263.   from = 0,
  264.   to = 6 * pi,
  265.   length.out = n_frames
  266. )
  267.  
  268. # -----------------------------------------------------------------------------
  269. # Generate the complete animation data set
  270. #
  271. # map2_dfr() iterates over two inputs at once:
  272. #
  273. #   1. times
  274. #   2. frame numbers
  275. #
  276. # For each pair, it calls:
  277. #
  278. #   make_frame(time, frame_number)
  279. #
  280. # The "_dfr" suffix means that all resulting data frames are row-bound into one
  281. # large data frame.
  282. #
  283. # The final data frame has:
  284. #
  285. #   number of rows = number of points * number of frames
  286. #
  287. # In this script:
  288. #
  289. #   10,000 * 180 = 1,800,000 rows
  290. # -----------------------------------------------------------------------------
  291.  
  292. df <- map2_dfr(
  293.   times,
  294.   seq_along(times),
  295.   make_frame
  296. )
  297.  
  298. # -----------------------------------------------------------------------------
  299. # Build the animated plot
  300. #
  301. # We draw two layers:
  302. #
  303. #   1. highlighted tips
  304. #      - larger
  305. #      - semi-transparent
  306. #      - colour oscillates between white and red
  307. #
  308. #   2. base/body points
  309. #      - smaller
  310. #      - white
  311. #
  312. # The colour column already contains literal colour names or RGB hex values.
  313. # Therefore, we use scale_colour_identity().
  314. # -----------------------------------------------------------------------------
  315.  
  316. p <- ggplot(df, aes(x, y)) +
  317.  
  318.   # ---------------------------------------------------------------------------
  319.   # Highlighted tip points
  320.   # ---------------------------------------------------------------------------
  321.  
  322.   geom_point(
  323.     data = filter(df, group == "tips"),
  324.     aes(colour = colour),
  325.     alpha = 0.5,
  326.     size = 2.2
  327.   ) +
  328.  
  329.   # ---------------------------------------------------------------------------
  330.   # Base/body points
  331.   # ---------------------------------------------------------------------------
  332.  
  333.   geom_point(
  334.     data = filter(df, group == "base"),
  335.     aes(colour = colour),
  336.     alpha = 0.75,
  337.     size = 0.45
  338.   ) +
  339.  
  340.   # Use colour values exactly as stored in the data
  341.   scale_colour_identity() +
  342.  
  343.   # ---------------------------------------------------------------------------
  344.   # Visible plotting window
  345.   #
  346.   # These limits reproduce the cropped view of the original animation.
  347.   #
  348.   # expand = FALSE removes extra padding around the plot.
  349.   # ---------------------------------------------------------------------------
  350.  
  351.   coord_cartesian(
  352.     xlim = c(100, 300),
  353.     ylim = c(75, 320),
  354.     expand = FALSE
  355.   ) +
  356.  
  357.   # Remove axes, ticks, labels and grid lines
  358.   theme_void() +
  359.  
  360.   # Black background
  361.   theme(
  362.     plot.background = element_rect(fill = "black", colour = NA),
  363.     panel.background = element_rect(fill = "black", colour = NA)
  364.   ) +
  365.  
  366.   # ---------------------------------------------------------------------------
  367.   # Animation instruction
  368.   #
  369.   # transition_manual(frame) tells gganimate:
  370.   #
  371.   #   use each distinct value of "frame" as one animation frame
  372.   # ---------------------------------------------------------------------------
  373.  
  374.   transition_manual(frame)
  375.  
  376. # -----------------------------------------------------------------------------
  377. # Render the animation
  378. #
  379. # This writes the animated GIF to the current working directory.
  380. #
  381. # You can check your working directory with:
  382. #
  383. #   getwd()
  384. #
  385. # You can change it with:
  386. #
  387. #   setwd("path/to/folder")
  388. # -----------------------------------------------------------------------------
  389.  
  390. animate(
  391.   p,
  392.   nframes = n_frames,
  393.   fps = fps,
  394.   width = 550,
  395.   height = 550,
  396.   renderer = gifski_renderer(output_file)
  397. )
  398.  
  399. ###############################################################################
  400. # Output
  401. #
  402. # The file:
  403. #
  404. #   living_animal.gif
  405. #
  406. # is created in the current working directory.
  407. #
  408. # On Linux, open it with:
  409. #
  410. #   xdg-open living_animal.gif
  411. #
  412. # or:
  413. #
  414. #   firefox living_animal.gif
  415. #
  416. ###############################################################################
  417.  
Tags: animation life
Advertisement
Add Comment
Please, Sign In to add comment