Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Last active February 27, 2017 18:46
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 JoshCheek/70d40475ba51af45a14795a225a19b32 to your computer and use it in GitHub Desktop.
Save JoshCheek/70d40475ba51af45a14795a225a19b32 to your computer and use it in GitHub Desktop.
Watching the planets animation
# video @ https://vimeo.com/205930710
# to play w/ it, see http://rayhightower.com/blog/2017/02/15/animated-graphics-in-ruby/
require 'graphics'
class Universe < Graphics::Simulation
def initialize
super 800, 600, 24
color.default_proc = -> h, k { k }
@tail_duration = 5 # How many frames to show their tail for
num_planets = 100 # Fewer allows more nuanced interactions, more keeps it from feeling empty
max_mass = 100 # their gravitational pull and radius are based on this
@num_interpopations = 10 # slows them down and smooths out their path (they insert thss many intermediate positions between every application of velocity)
@interpolations = []
@planets = num_planets.times.map do |i|
# get more planets with low weight, otherwise all the big planets pull each other too much
percent = 1 - Math.sqrt(1 - (i.to_f/num_planets.pred)**2)
Planet.new x: rand(w), y: rand(h), # position
vx: 0, vy: 0, # initial velocity to keep it interesting
mass: percent*max_mass+1,
color: [ # starting it here b/c they fade to black
50+rand(205), # so I don't want it too dim
50+rand(205),
50+rand(205),
]
end
@planets.sort_by! { |s| -s.radius } # so the small ones pass in front of the big ones instead of getting eclipsed
end
def update(n)
return unless @interpolations.empty?
# Every planet applies some force to every other planet
# I'm totally just making this math up based on what's likely to get
# something similar to what I want (small planits orbiting around big ones)
@planets.combination(2) do |planet1, planet2|
∆x = delta w, planet1.x, planet2.x
∆y = delta h, planet1.y, planet2.y
dist = Math.sqrt ∆x**2 + ∆y**2
force = planet1.force_on(dist) + planet2.force_on(dist)
mass = planet1.mass + planet2.mass
ø12 = angle_between(∆x, ∆y, dist)
ø21 = ø12 + Math::PI
mag1 = force * planet2.mass/mass
mag2 = force * planet1.mass/mass
# The clamp here limits their acceleration, because when they get too
# close to each other, (eg within 1 pixel), then they're dividing by
# almost zero, which leads to massive accellerations, which flings them
# so fast that they will pass by other planets too quickly to really
# experience its pull. This puts a limit on that
planet1.apply_force ø12, clamp(mag1, -1, 1)
planet2.apply_force ø21, clamp(mag2, -1, 1)
end
# # Force that pulls the biggest one towards the center (they are sorted, so
# # the biggest one is the first one) This is because they sometimes wind up
# # centering on an edge, which makes them jump back and forth at the extremes
# # of the screen.
# biggest = @planets.first
# ∆x = w/2 - biggest.x
# ∆y = h/2 - biggest.y
# mag = Math.sqrt(∆x**2+∆y**2)
# ø = angle_between ∆x, ∆y, mag
# biggest.apply_force ø, 1
@planets.each do |planet|
# Left / right screen wrap
if planet.x < 0
planet.x += w
elsif w < planet.x
planet.x -= w
end
# Top / bottom screen wrap
if planet.y < 0
planet.y += h
elsif h < planet.y
planet.y -= h
end
# If they're going too fast to interact, slow their velocity
# This one only slows it if they're going too fast up / right
# which will cause them to have more net pull towards bottom / left
# so they wind up all processing towards bot-left together and sort of
# swarming in and out, looks pretty cool :)
#
# Might be fun to instead limit it by the opposite of their tangent line
# then they should swarm in a circle around the middle
planet.vx *= 0.9 if planet.vx > 10
planet.vy *= 0.9 if planet.vy > 10
# This one just limits them from going too fast altogether
# if planet.vx**2 + planet.vy**2 > 100
# planet.vx *= 0.99
# planet.vy *= 0.99
# end
# Here is where we record the interpolations, eg if x was 10, and vx was
# 20, then their next x would be 30. If there were 4 interpolations, this
# would become x values of 15, 20, 25, 30, so slows it down and smooths it out
planet.interpolations(@num_interpopations).each_with_index do |xy, i|
(@interpolations[i] ||= []) << [planet, xy]
end
end
end
# finds the distance between d1 and d2, but allows them to wrap around max
def delta(max, d1, d2)
∆1 = d2 - d1
∆2 = max-d1+d2
∆best = ∆1.abs < ∆2.abs ? ∆1 : ∆2
∆3 = d2-max-d1
∆best = ∆best.abs < ∆3.abs ? ∆best : ∆3
∆best
end
# restrict val to be between min and maax
def clamp(val, min, max)
val = min if val < min
val = max if max < val
val
end
def draw(n)
clear
@future ||= []
# Applies the next interpolation and records the tail
@interpolations.shift.each do |planet, (x, y)|
planet.update x, y
@tail_duration.times do |i|
(@future[i] ||= []) << lambda {
r, g, b = planet.color
color = [r-r*i/@tail_duration, g-g*i/@tail_duration, b-b*i/@tail_duration]
circle x, y, planet.radius, color, true
}
end
end
# future[0] is the present, so remove it and draw it
@future.shift.each &:call
end
# # Center the screen on the biggest circle
# def circle(x, y, r, c, fill)
# biggest = @planets.first
# ∆x = w/2 - biggest.x
# ∆y = h/2 - biggest.y
# x += ∆x
# x %= w
# y += ∆y
# y %= h
# super x, y, r, c, fill
# end
# I feel like there's got to be a better way than this, but I wasn't finding
# it, so had to make my own. It receives the radius b/c the call sites have
# already calculated that, so have them pass it instead of recalculating it
def angle_between(∆x, ∆y, r)
ø = Math.asin(∆y/r)
if 0 <= ∆x && 0 <= ∆y
ø # quadrant 1
elsif 0 <= ∆x
ø + 2*Math::PI # quadrant 4
elsif 0 <= ∆y
Math::PI-ø # quadrant 2
else
Math::PI-ø # quadrant 3
end
end
end
# Possibly more stuff could move into this class,
# but I keep changing things across that line, so didn't want to make too many abstractions
class Planet
attr_accessor :color, :mass, :radius, :x, :y, :vx, :vy, :ax, :ay
def initialize(color:, mass:, x:, y:, vx: 0, vy: 0, ax: 0, ay: 0)
self.x, self.y, self.vx, self.vy = x, y, vx, vy
self.mass, self.color = mass, color
self.radius = (mass/10.0).ceil
end
def force_on(dist)
# if it gets inside the planet, push it away so that it doesn't get stuck there
# otherwise, draw it toward the planet with more force if it has more mass
# and less force as it has more distance
if dist <= 2*radius
- mass / dist
else
mass / dist
end
end
def apply_force(ø, magnitude)
self.vy += Math.sin(ø)*magnitude
self.vx += Math.cos(ø)*magnitude
end
def interpolations(n)
∆x = vx/n
∆y = vy/n
n.times.map { |i| [x+i*∆x, y+i*∆y] }
end
def update(x, y)
self.x = x
self.y = y
end
end
Universe.new.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment