Last active
February 27, 2017 18:46
-
-
Save JoshCheek/70d40475ba51af45a14795a225a19b32 to your computer and use it in GitHub Desktop.
Watching the planets animation
This file contains 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
# 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