-
-
Save zed/a011754fa77737e099b7 to your computer and use it in GitHub Desktop.
solar system animation using quickdraw in Python
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
RootObject: Sun | |
Object: Sun | |
Satellites: Mercury,Venus,Earth,Mars,Jupiter,Saturn,Uranus,Neptune,Ceres,Pluto,Haumea,Makemake,Eris | |
Radius: 20890260 | |
Orbital Radius: 0 | |
Object: Miranda | |
Orbital Radius: 5822550 | |
Radius: 23500 | |
Period: 1.413 | |
Object: Ariel | |
Orbital Radius: 8595000 | |
Radius: 60000 | |
Period: 2.520379 | |
Object: Umbriel | |
Orbital Radius: 11983500 | |
Radius: 60000 | |
Period: 4.144177 | |
Object: Titania | |
Orbital Radius: 19575000 | |
Radius: 75000 | |
Period: 8.7058 | |
Object: Oberon | |
Orbital Radius: 26235000 | |
Radius: 75000 | |
Period: 13.463 | |
Object: Uranus | |
Orbital Radius: 453572956 | |
Radius: 2555900 | |
Period: 30799 | |
Satellites: Puck,Miranda,Ariel,Umbriel,Titania,Oberon | |
Object: Neptune | |
Orbital Radius: 550000000 | |
Radius: 2476400 | |
Period: 60190 | |
Satellites: Triton | |
Object: Triton | |
Orbital Radius: 40000000 | |
Radius: 135300 | |
Period: -5.8 | |
Object: Mercury | |
Orbital Radius: 38001200 | |
Period: 87.9691 | |
Radius: 243900.7 | |
Object: Venus | |
Orbital Radius: 57477000 | |
Period: 224.698 | |
Radius: 605100.8 | |
Object: Earth | |
Orbital Radius: 77098290 | |
Period: 365.256363004 | |
Radius: 637100.0 | |
Satellites: Moon | |
Object: Moon | |
Orbital Radius: 18128500 | |
Radius: 173700.10 | |
Period: 27.321582 | |
Object: Mars | |
Orbital Radius: 106669000 | |
Period: 686.971 | |
Radius: 339600.2 | |
Satellites: Phobos,Deimos | |
Object: Phobos | |
Orbital Radius: 3623500.6 | |
Radius: 200000 | |
Period: 0.31891023 | |
Object: Deimos | |
Orbital Radius: 8346000 | |
Period: 1.26244 | |
Radius: 200000.2 | |
Object: Jupiter | |
Orbital Radius: 210573600 | |
Period: 4332.59 | |
Radius: 7149200 | |
Satellites: Io,Europa,Ganymede,Callisto | |
Object: Ceres | |
Orbital Radius: 130995855 | |
Period: 1679.67 | |
Radius: 48700 | |
Object: Io | |
Orbital Radius: 22000000 | |
Period: 1.7691377186 | |
Radius: 182100.3 | |
Object: Europa | |
Orbital Radius: 36486200 | |
Period: 3.551181 | |
Radius: 156000.8 | |
Object: Ganymede | |
Orbital Radius: 47160000 | |
Period: 7.15455296 | |
Radius: 263400 | |
Object: Callisto | |
Orbital Radius: 69700000 | |
Period: 16.6890184 | |
Radius: 241000 | |
Object: Saturn | |
Orbital Radius: 353572956 | |
Period: 10759.22 | |
Radius: 6026800 | |
Satellites: Mimas,Enceladus,Tethys,Dione,Rhea,Titan,Iapetus | |
Object: Mimas | |
Orbital Radius: 8433396 | |
Radius: 20600 | |
Period: 0.9 | |
Object: Enceladus | |
Orbital Radius: 10706000 | |
Radius: 25000 | |
Period: 1.4 | |
Object: Tethys | |
Orbital Radius: 13706000 | |
Radius: 50000 | |
Period: 1.9 | |
Object: Dione | |
Orbital Radius: 17106000 | |
Radius: 56000 | |
Period: 2.7 | |
Object: Rhea | |
Orbital Radius: 24000000 | |
Radius: 75000 | |
Period: 4.5 | |
Object: Titan | |
Orbital Radius: 50706000 | |
Radius: 257600 | |
Period: 15.945 | |
Object: Iapetus | |
Radius: 75000 | |
Orbital Radius: 72285891 | |
Period: 79 |
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
#!/usr/bin/env python | |
""" | |
See | |
http://stackoverflow.com/questions/13520435/python3-recursion-animation-in-quickdraw | |
""" | |
from __future__ import division | |
import logging | |
from itertools import groupby | |
from math import pi as PI, sin, cos | |
from timeit import default_timer as timer # performance counter | |
OrderedDict = dict | |
try: | |
from collections import OrderedDict | |
except ImportError: | |
pass | |
import quickdraw as qd | |
DAY = 86400 # day in seconds | |
class Clock(object): | |
def __init__(self): | |
self._time = timer() | |
def tick(self, fps): | |
"""Limit number of calls to fps calls per second.""" | |
endtime = self._time + 1 / fps | |
t = timer() | |
while t < endtime: # busy loop | |
t = timer() | |
elapsed = t - self._time | |
self._time = t | |
return elapsed | |
class Body(object): | |
def __init__(self, name, radius, orbital_radius, period, satellites): | |
self.name = name | |
self.radius = radius | |
self.orbital_radius = orbital_radius | |
self.period = period * DAY | |
self.satellites = satellites | |
self.phi = 0 | |
self.scr_radius = self.radius | |
self.scr_orbital_radius = self.orbital_radius | |
@classmethod | |
def from_file(cls, filename_or_file): | |
cls.bodies = {} | |
should_close = not hasattr(filename_or_file, 'read') # filename given | |
file = open(filename_or_file) if should_close else filename_or_file | |
try: | |
# find root object name | |
for line in file: | |
key, sep, value = line.partition(":") | |
if sep and key == "RootObject": | |
root_object_name = value.strip() | |
break | |
else: | |
raise ValueError("can't find root object name ") | |
# parse objects (bodies) | |
def start_object(line, start=[None]): | |
if line.startswith('Object:'): # start new object | |
start[0] = not start[0] | |
return start[0] | |
for _, object_ in groupby(file, key=start_object): | |
d = dict(line.partition(':')[::2] for line in object_) | |
if 'Object' not in d: | |
continue # skip empty group | |
body = Body( | |
name=d['Object'].strip(), | |
radius=float(d['Radius']), | |
orbital_radius=float(d.get('Orbital Radius') or 0), | |
period=float(d.get('Period') or .0), | |
satellites=d.get('Satellites', '').split(",") or []) | |
cls.bodies[body.name] = body | |
finally: | |
if should_close: | |
file.close() | |
# fix satellites | |
root = cls.bodies[root_object_name] | |
root.remove_undefined_satellites() | |
return root | |
def remove_undefined_satellites(self): | |
for i, sputnik in enumerate(self.satellites): | |
if not isinstance(sputnik, Body): | |
sputnik = Body.bodies.get(sputnik.strip()) | |
if sputnik is not None: | |
self.satellites[i] = sputnik | |
sputnik.remove_undefined_satellites() | |
# remove non-bodies | |
self.satellites[:] = [body for body in self.satellites | |
if isinstance(body, Body)] | |
def asdict(self): | |
return OrderedDict([ | |
('Object', self.name), | |
('Radius', self.radius), | |
('Orbital Radius', self.orbital_radius), | |
('Period', self.period), | |
('Satellites', [body.asdict() for body in self.satellites]), | |
]) | |
def update(self, time_): | |
"""Update x, y physical coordinates recursively.""" | |
if self.period: | |
self.rotate(time_ * 2 * PI / self.period) | |
for body in self.satellites: | |
body.update(time_) | |
def rotate(self, phi): | |
"""Rotate phi radians anticlock-wise.""" | |
self.phi += phi | |
self.phi %= 2 * PI | |
def scale(self, f): | |
# convert from physical scale to screen scale | |
self.scr_radius = f * self.radius | |
self.scr_orbital_radius = f * self.orbital_radius | |
for body in self.satellites: | |
body.scale(f) | |
def render(self, graphics, x0, y0, _level=1): | |
"""Draw the body recursively. | |
x0, y0 - screen coordinates of a central body (parent) | |
""" | |
# note: x0, y0 could be replaced by moving a group (begingroup, ..) | |
# compute screen coordinates | |
x = self.scr_orbital_radius * cos(self.phi) | |
y = -self.scr_orbital_radius * sin(self.phi) | |
# draw orbit | |
graphics.color(*[100] * 4) | |
graphics.circle(x0, y0, self.scr_orbital_radius) | |
# draw itself | |
if self.name == "Earth": | |
graphics.color(10, 255, 10) | |
else: | |
graphics.color(255 / _level, 255 / _level, 0, | |
255 - 10 * _level) | |
graphics.fillcircle(x + x0, y + y0, max(1, self.scr_radius)) | |
# draw satellites | |
for body in self.satellites: | |
body.render(graphics, x + x0, y + y0, _level + 1) | |
class Screen(object): | |
def __init__(self): | |
self.width, self.height = 800, 600 | |
def scale(self, maxdist): | |
return min(self.height, self.width) / maxdist / 2 | |
def max_distance(body): | |
if body.satellites: | |
maxr = max(body.radius, max(map(max_distance, body.satellites))) | |
else: | |
maxr = body.radius | |
return body.orbital_radius + maxr | |
def main(): | |
logging.basicConfig() # to see debug output; set level=logging.DEBUG | |
screen = Screen() | |
clock = Clock() | |
body = Body.from_file("input.txt") | |
maxdist = max_distance(body) | |
body.scale(screen.scale(maxdist)) | |
qd.flush(False) # don't update screen immediately | |
qd.windowevents(True) | |
qd.render('on') | |
elapsed = 0 | |
while True: | |
for e in qd.get_events(): | |
if e.type is qd.WindowResized: | |
screen.width, screen.height = e.value | |
body.scale(screen.scale(maxdist)) | |
# draw background | |
qd.color(0, 0, 0) | |
qd.clear() | |
# draw bodies recursively | |
body.update(1000000 * elapsed) | |
body.render(qd, screen.width // 2, screen.height // 2) | |
qd.refresh() # put on screen | |
elapsed = clock.tick(fps=60) # no more than given frames per second | |
main() |
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
"""Convert function calls into corresponding quickdraw commands. | |
It uses http://pages.cpsc.ucalgary.ca/QuickDraw/quickdraw.jar | |
>>> import quickdraw as qd | |
>>> qd.help() | |
>>> qd.color(255, 0, 0) | |
>>> qd.fillcircle(100, 100, 50) | |
""" | |
# Implementation is based on | |
# https://github.com/amoffat/sh/blob/master/sh.py | |
import logging | |
import sys | |
from collections import deque, namedtuple | |
from subprocess import Popen, PIPE | |
from threading import Thread | |
debug = logging.getLogger('quickdraw').debug | |
class Error(RuntimeError): | |
pass | |
Event = namedtuple('Event', "type value") | |
class WindowResized(Event): | |
__slots__ = () | |
def __new__(cls, value): | |
return Event.__new__(cls, WindowResized, | |
[int(n) for n in value.split(",")]) | |
class WindowStateChanged(Event): | |
__slots__ = () | |
def __new__(cls, value): | |
return Event.__new__(cls, WindowStateChanged, value.strip()) | |
WindowStateChangedClosed = WindowStateChanged('Closed') | |
class EventType: | |
WindowResized = WindowResized | |
WindowStateChanged = WindowStateChanged | |
class Module(object): | |
"""Allow to import arbitrary quickdraw commands.""" | |
def __init__(self, module): | |
self._module = module | |
def __getattribute__(self, name): | |
if name == '_module': | |
return object.__getattribute__(self, name) | |
elif ((name.startswith("__") and name.endswith("__")) or | |
name in "get_events WindowResized WindowResized".split() | |
): | |
return getattr(self._module, name) | |
else: # quickdraw command | |
return getattr(get_quickdraw(), 'command_' + name) | |
def __setattr__(self, name, value): | |
obj = self if name == '_module' else self._module | |
object.__setattr__(obj, name, value) | |
class Quickdraw(object): | |
"""quickdraw subprocess.""" | |
def __init__(self, quickdraw_cmd=("java", "-jar", "quickdraw.jar")): | |
self._process = Popen(quickdraw_cmd, | |
universal_newlines=True, | |
stdin=PIPE, stdout=PIPE, stderr=PIPE, | |
bufsize=1) # line-buffered | |
if sys.platform.startswith('java'): # jython support | |
self._events = deque() | |
else: | |
self._events = deque(maxlen=100) # limit event history size | |
start_daemon_thread(target=self._read_stdout) | |
self._error = None | |
start_daemon_thread(target=self._read_stderr) | |
def __getattr__(self, name): | |
if not name.startswith('command_'): | |
raise AttributeError(name) | |
cmd = self._make_command(name[len('command_'):]) | |
setattr(self, name, cmd) | |
return cmd | |
def _make_command(self, name): | |
def command(*args): | |
self._check_error() | |
cmd = name + " " + " ".join(quote(str(obj)) for obj in args) | |
debug(cmd) | |
try: | |
self._process.stdin.write(cmd) | |
self._process.stdin.write("\n") | |
except EnvironmentError: | |
if self._process.poll() is not None: | |
self.quit() | |
else: | |
raise | |
command.__name__ = name | |
return command | |
def get_events(self): | |
"""Yield all events occurred since previous call.""" | |
self._check_error() | |
while self._events: | |
yield self._events.popleft() | |
self._check_error() | |
def quit(self): | |
for stream in "stdin stdout stderr".split(): | |
s = getattr(self._process, stream) | |
if s is not None: | |
try: | |
s.close() | |
except EnvironmentError: | |
pass | |
setattr(self._process, stream, None) | |
sys.exit() | |
def _read_stdout(self): | |
try: | |
for line in iter(self._process.stdout.readline, ''): | |
debug(line) | |
type_, sep, value = line.partition(':') | |
if sep: | |
event_type = getattr(EventType, type_, None) | |
if event_type: | |
event = event_type(value) | |
if event == WindowStateChangedClosed: | |
self._error = 'quit' | |
self._events.append(event) | |
continue | |
sys.stdout.write(line) | |
finally: | |
self._process.stdout.close() | |
def _read_stderr(self): | |
self._error = self._process.stderr.read(1) | |
def _check_error(self): | |
if not self._error: | |
return | |
if self._error == 'quit': | |
self.quit() | |
c, self._error = self._error, None | |
message = c + self._process.stderr.readline() | |
raise Error(message) | |
def get_events(): | |
return get_quickdraw().get_events() | |
def start_daemon_thread(**thread_args): | |
t = Thread(**thread_args) | |
t.daemon = True | |
t.start() | |
def quote(s): | |
"""Quote string for quickdraw.""" | |
s = s.replace('\\', '\\\\').replace('"', r'\"') | |
return '"' + s + '"' if any(c.isspace() for c in s) else s | |
_quickdraw = None | |
def get_quickdraw(): | |
"""Return global quickdraw instance.""" | |
global _quickdraw | |
if _quickdraw is None: | |
_quickdraw = Quickdraw() | |
return _quickdraw | |
if __name__ == "__main__": | |
import doctest | |
sys.exit(doctest.testmod().failed) | |
else: | |
sys.modules[__name__] = Module(sys.modules[__name__]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment