Skip to content

Instantly share code, notes, and snippets.

@houhoulis
Last active March 18, 2023 01:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save houhoulis/42434f9e59b66241f874f9e769974969 to your computer and use it in GitHub Desktop.
Save houhoulis/42434f9e59b66241f874f9e769974969 to your computer and use it in GitHub Desktop.
Snowflakes falling in terminal in Ruby
# Seen on https://connectified.com/@ruby_discussions@mastodon.social/109993732600145441.
# Fixed data structure bug, added drift & other changes/improvements.
# Ruby version doing the same calculations as the elixir version in snow.exs
HEIGHT = `tput lines`.to_i
WIDTH = `tput cols`.to_i
SLEEP_DURATION = 0.5
LIFETIME = HEIGHT + 30
WIND_SPEED = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0].sample * [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0].sample / 4.21
FLAKES_PER_ROW = 5
def clear_screen = puts "\e\[2J"
def string_to_move_cursor(x, y) = "\e\[#{y};#{x}H"
def print_cursor_top_left
print "#{string_to_move_cursor(0, 0)}#{Time.now.strftime('%Y-%m-%d %H:%M:%S.%6N')}\nwind: #{WIND_SPEED.round(4)}#{string_to_move_cursor(0, 0)}"
end
def print_position(x, y, string)
if x >= 0 and x < WIDTH and y >= 0 and y < HEIGHT
print "#{string_to_move_cursor(x, y)}#{string}"
print_cursor_top_left
end
end
def erase_position(x, y)
if x >= 0 and x < WIDTH and y >= 0 and y < HEIGHT
print_position(x, y, " ")
print_cursor_top_left
end
end
def twinkle_print_position(x, y)
if rand < 0.94
print_position(x, y, colorless_flake)
else
print_position(x, y, colorful_flake)
end
end
def colorless_flake
# "\e[37m*\e[0m" is grey. Distinctive in some color configs and nigh-indistinguishable in others
["*", "*", "*", "*", "\e[37m*\e[0m"].sample
end
def colorful_flake
# red is too vibrant: "\e[31m*\e[0m"
["\e[32m*\e[0m", "\e[33m*\e[0m", "\e[34m*\e[0m", "\e[35m*\e[0m", "\e[36m*\e[0m"].sample
end
trap("SIGINT") { puts "Bye!" ; exit! }
def run
clear_screen
snowfall([])
end
def snowfall(flakes)
loop do
FLAKES_PER_ROW.times {
# the calculated area extends off-screen to the left and right
flakes << {x: rand(3 * WIDTH) - WIDTH - 1, y: 0, lifetime: LIFETIME}
}
flakes.map! do |flake|
x, y, lifetime = flake[:x], flake[:y], flake[:lifetime]
# Erase flake.
erase_position(x, y) unless lifetime == LIFETIME # newly-born, hasn't been drawn yet.
lifetime -= 1
# Only keep & process flakes that haven't gone too wide or reached the end of their lifetime.
if x >= -WIDTH && x < 2 * WIDTH && lifetime > 0
# Calculate wind- and random-drifted new x position, and extra drop in y position,
# as long as the flake hasn't reached the ground yet.
if y < HEIGHT - 1
random_horizontal_drift = rand
x = random_horizontal_drift < 0.1 ? x-1 : random_horizontal_drift > 0.9 ? x+1 : x
x += 1 if WIND_SPEED > 0 && rand < WIND_SPEED
x -= 1 if WIND_SPEED < 0 && rand < 0 - WIND_SPEED
# A slight chance to drift one extra unit lower.
y += 1 if rand < 0.1
end
# Fall the usual one unit if the flake still hasn't reached the ground yet.
y += 1 if y < HEIGHT - 1
twinkle_print_position(x, y)
{x: x, y: y, lifetime: lifetime}
end
end.compact!
sleep SLEEP_DURATION
end
end
run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment