Simple Simulation: Planets in the Solar System
Animations and transformations make it easy to build graphical simulations. This example is an orrery, a simulation of the planets in the solar system.
Understanding the Code
The best place to start is the class CircularOrbitingPlanet (Figure 1), which animates a simulation of a planet orbiting the sun in a circular orbit. The planet is drawn as a circle at the left of its orbit, which is the point with coordinates (sunCenterX - orbit, sunCenterY). Then an animation rotates that circle around the sun through the transformation Transform.rotate(angle, sunCenterX, sunCenterY). The bind keyword ensures that this transformation is reapplied anytime angle changes. (It would also be reapplied if the sunCenterX or sunCenterY coordinate changed, but neither one does.) The angle variable changes because of the timer, which arranges for it to sweep through all the values from 0 to 360 degrees, a complete circle.
public def orbit = bind planet.orbit * earthOrbitPixels;
init {
timer.play();
children = [
// Draw orbit circle:
Circle {
centerX: sunCenterX,
centerY: sunCenterY
radius: bind orbit
strokeWidth: solarsystem.ConstantPlanets.CONT_PADDING;
stroke: solarsystem.ConstantPlanets.orbitColor;
fill: null
},
// Draw planet name, but not if the orbit is small:
Label {
text: planet.name
textFill: planet.brightSideColor
layoutX: sunCenterX
layoutY: bind sunCenterY - orbit * 0.85
font: bind Font {
size: orbit / solarsystem.ConstantPlanets.APP_PADDING
}
visible: bind (orbit > 60)
},
// Draw planet, with animation:
Circle {
transforms: bind Transform.rotate(angle, sunCenterX, sunCenterY)
centerX: bind sunCenterX - orbit, centerY: sunCenterY
radius: bind planet.radius * earthRadius
fill:LinearGradient {
endX: 1.0,
endY: 0.0
stops: [
Stop {
offset: 0.0,
color: planet.darkSideColor
},
Stop {
offset: 1.0,
color: planet.brightSideColor
}
]
}
effect:Lighting {
light: PointLight {
x: sunCenterX,
y: sunCenterY,
z: solarsystem.ConstantPlanets.CIRCLE_EFFECT,
}
}
}
]
}
Figure 1: CircularOrbitingPlanet Class
Sliders
The speed of the animation can be controlled by the Speed slider. The rate variable in the Timeline is bound to the global rate variable, which in turn is bound to a fraction of the slider value. When you move the slider, rate changes, so the animation speeds up or slows down.
Similarly, the zoom effect is achieved by arranging for the dimensions of the zoomable objects to be multiples of scaleFactor, and binding scaleFactor to the value of the Zoom slider. In Figure 1, earthOrbitPixels and earthRadius are scaled in this way. Figure 2 shows how scaleFactor is defined.
def zoomSliderMax = 200; def zoomSliderDefault = 5; def zoomSlider = SwingSlider {maximum: zoomSliderMax, value: zoomSliderDefault}; var scaleFactor = bind (80.0 * zoomSlider.value) / zoomSliderMax;
Figure 2: scaleFactor Bound to the Value of the Zoom Slider
Effects for the Planets
The planets are drawn by using two effects. The LinearGradient effect produces a gradual shading from the color of the bright side to the color of the dark side. The bright side, of course, is the side facing the sun. Because the initial position of the planet is at the left of its orbit, the shading changes from darkSideColor at x = 0.0 to lightSideColor at x = 1.0. When the planet is rotated in its orbit, the shading rotates with it, so the bright side is always facing the sun.
The second effect is a PointLight that is situated 300 pixels from the sun in the Z direction. In other words, this light is off the screen, so it can illuminate the planets from above. This effect gives a three-dimensional look to the planets.
Elliptical Orbits
In reality, the orbits of the planets are ellipses, not circles. But for many of them, the difference is very slight. For example, the earth is only 3% farther from the sun at its farthest point than at its nearest point. For these planets, the orrery shows a circular orbit, using the CircularOrbitingPlanet class shown in Figure 1.
For planets that have more elliptical orbits, the class EllipticalOrbitingPlanet is used instead. This class follows Kepler's laws to shift the orbit appropriately, and to make it move faster when it is nearer the sun. But the orbit is still drawn as a circle, because even the most elliptical orbit (Mercury's) is only 2-percent wider than it is tall.
Instead of a simple Timeline with just the endpoints of 0 and 360 degrees, the orbit time is divided into 72 equal slices and the correct angle is computed for each slice. These computed values are put into a KeyFrame array for the Timeline. The outline of the code is in Figure 3.
class EllipticalOrbitingPlanet extends Container {
...
def nSubPeriods = 72;
var angle: Number;
var timer: Timeline;
init {
...
var frames: KeyFrame[];
...
for (t in [0..nSubPeriods]) {
def tFraction = (t as Number) / nSubPeriods;
...computations...
// Now convert those polar coordinates into cartesian ones...
def x = -offsetFromCentre - r * Math.cos(theta);
def y = -r * Math.sin(theta);
// ...and back into an angle relative to the centre of the ellipse
def centreAngle = 180 + Math.toDegrees(Math.atan2(y, x));
insert
KeyFrame {
time: period.mul(tFraction)
values: [ angle => centreAngle ]
}
into frames;
}
time: period
values: [ angle => 360 ]}
into frames;
timer = Timeline {
repeatCount: Timeline.INDEFINITE,
rate: bind solarsystem.CircularOrbitingPlanet.speed,
keyFrames: frames
};
timer.play();
...
children = [orbCircle,lableplanetname,circletransform]
}
Figure 3: Elliptical Orbit Computed in 72 Slices
Incidentally a certain amount of duplication exists between the classes CircularOrbitingPlanet and EllipticalOrbitingPlanet. The classes could be refactored, but then this code sample would be less clear.
Physical Realism
It is not feasible in an orrery for the planets to be on the same scale as their orbits. For example, the earth is about 13,000 km wide, but its orbit is 300,000,000 km wide, or 23,000 times as much. So, even if the orbit filled up the entire screen, the earth would be only a tiny fraction of a pixel.
Instead, the planets are vastly magnified relative to their orbits. However, they are in proportion relative to one another. For example, Jupiter is 11 times as wide as the earth in reality and also in the orrery. The planets are also magnified relative to the sun: in reality, the sun is 100 times wider than Jupiter, but in the orrery it appears smaller.
The orbits of the planets are also in proportion relative to one another. That is why, on a scale where the outermost planet (Neptune) is visible, the innermost planets are tiny.
All of the elliptical orbits are shown in the same orientation, with the wide axis of the ellipse being horizontal and the sun nearer the left end. In reality, these ellipses are not aligned with each other. In fact each one rotates around the sun over the millennia (a phenomenon known as apsidal precession), due to gravitational attraction between the planets.
Eamonn McManus