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 =
  loop
    (* 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
    in
    let vy =
      if box.bot < pre_y && pre_y < box.top then pre_vy
      else -. pre_vy
    in
    let x, y = (pre_x +. vx, pre_y +. vy) in

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

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