Causing Interaction Between Polar-Opposite Magnetic Balls

By Sergey Malenkov, December 15, 2008

This JavaFX application implements a simple physical model of an interaction between several magnetized balls with opposite polarity. An attractive force and the mass of a ball are directly proportional to the ball's radius. Dragging the ball with a mouse produces an acceleration.

Understanding the Code

The Ball class, the custom node for a single ball, provides functions for moving the ball and interaction with other balls. The Main class creates several balls that start moving according to their polarity.

The event variable is used for calculating the difference between new and old positions of the ball and moving it to the new position. Note that this difference produces an acceleration of the ball when the mouse is released after dragging. The code shown in Figure 1 provides this functionality.

Source Code
var event: MouseEvent on replace old {
  if (event != null) {
    vx = event.dragX - old.dragX;
    vy = event.dragY - old.dragY;
    translateX += vx;
    translateY += vy;
  }
}
...
    cursor: Cursor.HAND
    onMouseDragged: function(event) {
      this.event = event;
    }
    onMouseReleased: function(event) {
      this.event = null;
    }
  

Figure 1: Mouse Dragging the Ball

The function shown in Figure 2 updates the ball position and validates it according to the bound of the scene. It is called 25 times per second after processing the interaction with other balls.

Source Code
public function update() {
  if (event == null) {
    translateX += (vx *= 0.9);
    translateY += (vy *= 0.9);
    if (translateX < radius) { vx = -vx;  translateX = radius + radius - translateX }
    if (translateY < radius) { vy = -vy;  translateY = radius + radius - translateY }
    var w = scene.width  - radius;
    var h = scene.height - radius;
    if (translateX > w) { vx = -vx;  translateX = w + w - translateX }
    if (translateY > h) { vy = -vy;  translateY = h + h - translateY }
  }
}
  

Figure 2: Ball Movement

The function shown in Figure 3 processes the collision of two balls and updates their velocity according to their polarity. The function is called 25 times per second for every pair of balls on the scene.

Source Code
public function process(ball: Ball) {
  var d  = ball.radius + radius;
  var dx = ball.translateX - translateX;
  var dy = ball.translateY - translateY;
  var dd = (dx * dx) + (dy * dy);
  var collision = dd <= (d * d);
  // process collision
  if (collision) {
    var dvx = ball.vx - vx;
    var dvy = ball.vy - vy;

    var mag = dvx * dx + dvy * dy;
    if (mag <= 0) {
      mag /= dd;
    
      var delta_vx = dx * mag;
      var delta_vy = dy * mag;

      ball.vx -= delta_vx * radius / d;
      ball.vy -= delta_vy * radius / d;

      vx += delta_vx * ball.radius / d;
      vy += delta_vy * ball.radius / d;
    }
  }
  // process gravity
  if (dd > 0) {
    var f = 0.05 * ball.radius * radius / dd;

    var delta_vx = dx * f;
    var delta_vy = dy * f;
    if (collision or (ball.sign == sign)) {
      delta_vx = -delta_vx;
      delta_vy = -delta_vy;
    }

    ball.vx -= delta_vx * radius / d;
    ball.vy -= delta_vy * radius / d;

    vx += delta_vx * ball.radius / d;
    vy += delta_vy * ball.radius / d;
  }
}
  

Figure 3: Interaction of the Balls

To summarize, Figure 4 shows the animation that processes the balls' interaction and movement every 40 ms to display 25 frames per second.

Source Code
Timeline {
  repeatCount: Timeline.INDEFINITE
  keyFrames: KeyFrame {
    time: 40ms
    action: function() {
      var n = scene.content.size() - 2;
      for (i in [0..n]) {
        def ball = scene.content[i] as Ball;
        for (j in [i..n]) {
           ball.process(scene.content[j+1] as Ball)
        }
      }
      for (node in scene.content)
        (node as Ball).update()
    }
  }
}.play()
  

Figure 4: Recalculating 25 Times per Second

Customizing the Code

To further customize the sample, change the preferred width and height of the scene in the Main class, as shown in Figure 5. For example, you can change a background color or apply a gradient fill effect.

Source Code
def scene = Scene {
  fill: Color.BLACK
  width:  600
  height: 400
}
  

Figure 5: Scene Initialization

The code shown in Figure 6 creates 10 different balls by using random values for the position, radius, and the sign of polarity.

Source Code
scene.content = for (i in [1..10]) Ball {
  sign: 0.5 < rnd.nextDouble()
  radius: 10 + 20 * rnd.nextDouble()
  translateX: scene.width  * rnd.nextDouble()
  translateY: scene.height * rnd.nextDouble()
}
  

Figure 6: Creating Magnetic Balls

Note that the scene content is created after the stage initialization. It is necessary for obtaining the actual width and height of the scene, which are used to generate the start position of the balls.