Last active
August 19, 2017 23:21
-
-
Save xunker/540269d8e86f3d44040f2b54de9018a9 to your computer and use it in GitHub Desktop.
Ruby eclipse simulator and sun/moon position visualizer using Gosu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env ruby | |
| # | |
| # Visualizer to show sun and moon positions, give a certain time and position on | |
| # the earth. | |
| # | |
| # Uses "gosu" so you will need to install the following libraries: | |
| # * gosu (brew install gosu on macos) | |
| # * sdl2 (brew install sdl2 on macos) | |
| # | |
| # Uses gems: | |
| # * suncalc | |
| # * gosu | |
| # The moon is the brighter of the two bodies. The sun is the dimmer of the two. | |
| # | |
| # The green line is the horizon and will move up and down depending on the time | |
| # of the year. | |
| # | |
| # Default starting position on the earth is Grand Teton National Park, which | |
| # will be in the path of totality for the 2017 Eclipse. Default time is the | |
| # projected time of totality for that location. | |
| # | |
| # You can move through time with the left and right arrows. By default, right | |
| # arrow will move you 1 hour ahead, left will move you one hour behind. You can | |
| # change the movement speed using the up and down arrows. Your current movement | |
| # speed is shown in the window title bar. | |
| # | |
| # The "r" key will set the time to be sunrise of the current day. | |
| # | |
| # The "s" key will set the time to be sunset of the current day. | |
| # | |
| # Escape will exit. | |
| # | |
| # You can change the position on earth by changing the value of `@coords` | |
| # defined inthe Eclipse#initialize method. Insert your prefered Lat/Long | |
| # coordinates in decimal format. | |
| # | |
| # You can also change the default start time by changing `@current_time` | |
| # defined in the Eclipse#initialize method. You can also uses the SunCalc gem | |
| # to set the time to be sunrise, sunset, mid-day, etc. | |
| require 'suncalc' | |
| require 'time' | |
| require 'gosu' | |
| # brew install gosu | |
| # brew install sdl2 | |
| # https://gist.github.com/jlnr/661266 | |
| class Circle | |
| attr_reader :columns, :rows | |
| def initialize radius, color = 255 | |
| @columns = @rows = radius * 2 | |
| lower_half = (0...radius).map do |y| | |
| x = Math.sqrt(radius**2 - y**2).round | |
| right_half = "#{color.chr * x}#{0.chr * (radius - x)}" | |
| "#{right_half.reverse}#{right_half}" | |
| end.join | |
| @blob = lower_half.reverse + lower_half | |
| @blob.gsub!(/./) { |alpha| "#{255.chr}#{255.chr}#{255.chr}#{alpha}"} | |
| end | |
| def to_blob | |
| @blob | |
| end | |
| end | |
| class Eclipse < Gosu::Window | |
| MOVEMENTS_MODES = [ | |
| # name, times, change in seconds * times | |
| ['1 Minute', 1, 60], | |
| ['1 Hour', 60, 60], | |
| ['1 Day', 24, 3600], | |
| ['30 Days', 30, 86400], | |
| ['365 Days', 365, 86400] | |
| ] | |
| def initialize | |
| @width = 1024 | |
| @height = 746 | |
| super @width, @height | |
| self.caption = "Eclipse" | |
| @moon_d = 24 | |
| @moon = Gosu::Image.new(self, Circle.new(@moon_d, 255), false) | |
| @sun_d = 25 | |
| @sun = Gosu::Image.new(self, Circle.new(@sun_d, 128), false) | |
| @you_d = 10 | |
| @you = Gosu::Image.new(self, Circle.new(@you_d, 200), false) | |
| # @coords = [40.768860, -111.893273] # Salt Lake City, Utah | |
| @coords = [43.833333, -110.700833] # grand teton NP, 11:36am | |
| @current_time = Time.parse('2017-08-21 10:36:00') # grand teton NP totality, in MST/MDT time | |
| # @current_time = SunCalc.get_times(Time.now, @coords.first, @coords.last)[:solar_noon] | |
| # @current_time = SunCalc.get_times(Time.now, @coords.first, @coords.last)[:sunrise] | |
| # @current_time = SunCalc.get_times(Time.now, @coords.first, @coords.last)[:sunset] | |
| # @current_time = SunCalc.get_times(Time.parse('June 21, 2018'), @coords.first, @coords.last)[:sunrise] | |
| # Can use one of: [:solar_noon, :nadir, :sunrise, :sunset, :sunrise_end, :sunset_start, :dawn, :dusk, :nautical_dawn, :nautical_dusk, :night_end, :night, :golden_hour_end, :golden_hour] | |
| @current_movement_mode = 1 | |
| update_coords | |
| update_horizon_y | |
| puts 'update_horizon_y' | |
| @counter = 0 | |
| end | |
| def button_down(id) | |
| return if @moving | |
| case id | |
| when Gosu::Button::KbRight | |
| @moving = true | |
| puts "Forward in time #{MOVEMENTS_MODES[@current_movement_mode][0]}" | |
| @counter = MOVEMENTS_MODES[@current_movement_mode][1] | |
| when Gosu::Button::KbLeft | |
| @moving = true | |
| puts "Backward in time #{MOVEMENTS_MODES[@current_movement_mode][0]}" | |
| @counter = -MOVEMENTS_MODES[@current_movement_mode][1] | |
| when Gosu::Button::KbUp | |
| @current_movement_mode += 1 | |
| @current_movement_mode = 0 if @current_movement_mode > MOVEMENTS_MODES.length-1 | |
| when Gosu::Button::KbDown | |
| @current_movement_mode -= 1 | |
| @current_movement_mode = MOVEMENTS_MODES.length-1 if @current_movement_mode < 0 | |
| when Gosu::Button::KbR | |
| @current_time = get_times[:sunrise] | |
| when Gosu::Button::KbS | |
| @current_time = get_times[:sunset] | |
| when Gosu::Button::KbEscape | |
| exit | |
| end | |
| end | |
| def get_times | |
| SunCalc.get_times(@current_time, @coords.first, @coords.last) | |
| end | |
| # Sets horizon Y coordiate to be same as suns Y position at sunrise | |
| def update_horizon_y | |
| sunrise_time = get_times[:sunrise] | |
| sunrise_coords = SunCalc.get_position(sunrise_time, @coords.first, @coords.last) | |
| @horizon_y = map_range([-180, +180], [@height-@sun_d/2, @sun_d], sph2cart(sunrise_coords[:azimuth], sunrise_coords[:altitude])[:y]).to_i | |
| end | |
| def update_coords | |
| @sun_coords = SunCalc.get_position(@current_time, @coords.first, @coords.last) | |
| @moon_coords = SunCalc.get_moon_position(@current_time, @coords.first, @coords.last) | |
| end | |
| def update | |
| if @counter != 0 | |
| update_horizon_y | |
| update_caption | |
| end | |
| if @counter < 0 | |
| @current_time -= MOVEMENTS_MODES[@current_movement_mode][2] | |
| @counter += 1 | |
| elsif @counter > 0 | |
| @current_time += MOVEMENTS_MODES[@current_movement_mode][2] | |
| @counter -= 1 | |
| elsif @counter == 0 && @moving | |
| update_horizon_y | |
| @moving = false | |
| end | |
| update_coords | |
| end | |
| def draw | |
| # draw horizon | |
| draw_line(0, @horizon_y, Gosu::Color::GREEN, @width, @horizon_y, Gosu::Color::GREEN) | |
| # draw "you" | |
| @you.draw(@width/2, @horizon_y-@you_d, 0) | |
| # Draw sun sphere - the dimmer of the two | |
| @sun.draw( | |
| map_range([-180, +180], [@width-@sun_d/2, @sun_d], sph2cart(@sun_coords[:azimuth], @sun_coords[:altitude])[:x]).to_i-(@sun_d/2), | |
| map_range([-180, +180], [@height-@sun_d/2, @sun_d], sph2cart(@sun_coords[:azimuth], @sun_coords[:altitude])[:y]).to_i-(@sun_d/2), | |
| 0 | |
| ) | |
| # Draw moon sphere - the brighter of the two | |
| @moon.draw( | |
| map_range([-180, +180], [@width-@moon_d/2, @moon_d], sph2cart(@moon_coords[:azimuth], @moon_coords[:altitude])[:x]).to_i-(@moon_d/2), | |
| map_range([-180, +180], [@height-@moon_d/2, @moon_d], sph2cart(@moon_coords[:azimuth], @moon_coords[:altitude])[:y]).to_i-(@moon_d/2), | |
| 0 | |
| ) | |
| unless @moving | |
| # To prevent needless rapid redrawing if the screen isn't changing | |
| sleep(0.25) | |
| update_caption | |
| end | |
| end | |
| def update_caption | |
| self.caption = "#{(@current_time)} - #{MOVEMENTS_MODES[@current_movement_mode][0]}" | |
| end | |
| # Maps a number from one numeric range to another. In this case we need to be | |
| # able to map the coordinates of the sun/moon in to the coordinate range of | |
| # the Gosu window. | |
| def map_range(a, b, s) | |
| af, al, bf, bl = a.first, a.last, b.first, b.last | |
| bf + (s - af)*(bl - bf).quo(al - af) | |
| end | |
| # Convert Spherical coordinates (azimuth/elevation) in to cartesian (x/y) coordinates | |
| # https://www.mathworks.com/help/matlab/ref/sph2cart.html?requestedDomain=www.mathworks.com#input_argument_d0e929631 | |
| def sph2cart(azimuth, elevation) | |
| { | |
| # x: 180 * Math.cos(elevation) * Math.cos(azimuth), | |
| # y: 180 * Math.cos(elevation) * Math.sin(azimuth) | |
| x: 180 * Math.cos(elevation) * Math.sin(azimuth), | |
| y: 180 * Math.cos(elevation) * Math.cos(azimuth) | |
| } | |
| end | |
| end | |
| Eclipse.new.show |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment