Lesson 2: Moving ball

In this lesson, we will program a ball which bounces against the edges of a box, and which is duplicated on every emission of a signal split split. Here is an example of what we aim to achieve: lessons/lesson2/split.rml

We first define the data structure to represent the bounds of a box.

type box =
    { left: float;
      right: float;
      top: float;
      bot: float; }

We create a box.

let box =
  { left = 0.; right = 400.;
    bot = 0.; top = 400.; }

We display the box.

let () =
  let g =
    " " ^
    (string_of_int (int_of_float (box.right -. box.left))) ^
    "x" ^
    (string_of_int (int_of_float (box.top -. box.bot)))
  Graphics.open_graph g

We now define the data structure to represent the state of a ball.

type state =
    { pos: (float * float, float * float) event;
      speed: (float * float, float * float) event;
      radius: float;
      color: Graphics.color; }

It is a record whose fields pos, speed, radius and color represent respectively the position, velocity, radius and color of a ball.

The type of the field pos is (float * float, float * float) event. That is, it is an event on which we can emit and receive a tuple of floating numbers. It will represent the flow of positions.

To observe the balls, we use a global signal named draw on which each ball will emit its state. All the emitted states are collected into a list.

signal draw default [] gather (fun x y -> x :: y) ;;

The behavior of a ball bouncing into the limit of the box can be programmed as follows.

let process move state =
    (* emit the position *)
    emit draw state;

    (* compute the new position *)
    let pre_vx, pre_vy = last ?state.speed in
    let pre_x, pre_y = last ?state.pos in
    let vx =
      if box.left < pre_x && pre_x < box.right then pre_vx
      else -. pre_vx
    let vy =
      if box.bot < pre_y && pre_y < box.top then pre_vy
      else -. pre_vy
    let x, y = (pre_x +. vx, pre_y +. vy) in

    (* update the state *)
    emit state.speed (vx, vy);
    emit state.pos (x, y);

The process is an infinite loop that first emits the current state, then computes the new position and then finally updates the state.

Let us now create a state. To do that, we have to define auxiliary functions. The first one associates a color to an integer.

let color_of_int n =
  match n mod 7 with
  | 0 -> Graphics.rgb 220 20 60
  | 1 -> Graphics.blue
  | 2 -> Graphics.rgb 34 139 34
  | 3 -> Graphics.red
  | 4 -> Graphics.rgb 150 150 150
  | 5 -> Graphics.black
  | 6 -> Graphics.magenta
  | _ -> Graphics.black

The second function creates a vector of norm k.

let random_speed k =
  let alpha = Random.float 7. in
  (k *. cos alpha, k *. sin alpha)

Now, a function which creates a value of type state can be defined as follows.

let new_state () =
  signal pos
    default ((box.right -. box.left) /. 2., (box.top -. box.bot) /. 2.)
    gather (fun x _ -> x)
  signal speed default random_speed 2. gather (fun x _ -> x) in
  let color = color_of_int (Random.int 7) in
  { pos = pos; speed = speed; radius = 25.; color = color; }

The default value of the signal pos is the center of the box. The combination function only keeps one of the values emitted during an instant.

Let us create a ball:

#run (move (new_state ())) ;;

To observe the position of the ball, we program a process which displays the value of the draw signal.

let process window =
    await draw (all) in
      (fun state ->
        let x, y = last ?state.pos in
        Graphics.set_color state.color;
          (int_of_float x) (int_of_float y)
          (int_of_float state.radius))

#run window ;;

Now we want to create a ball which is duplicated each time a signal split is emitted.

signal split default () gather (fun () () -> ()) ;;

We first define a function which creates a new state from an existing one.

let new_state' state =
  signal pos default last ?state.pos gather fun x _ -> x in
  signal speed default random_speed 2. gather fun x _ -> x in
  let radius = max 1. (state.radius -. state.radius /. 5.) in
  let color = color_of_int (Random.int 7) in
  { pos = pos; speed = speed; radius = radius; color = color; }

Dynamic creation is achieved by combining recursion and parallel composition.

let rec process ball state =
  do run (move state)
  until split ->
    run (ball (new_state' state))
    run (ball (new_state' state))

#run (ball (new_state ())) ;;

Finally, each time we emit the split event, the ball is duplicated.

emit split ;;

emit split ;;

Warning the JavaScript interpreter can stop if there is to many balls.