Chapter 25
The beacons
Every radio tower has a magenta beacon on top, and the beacons blink. A beacon is the simplest moving thing in the ride: its brightness depends on two things only, which beacon it is and the clock.
The picture stands two towers side by side and plays two whole blinks, from step 5,400 to step 5,640. Their beacons blink opposite each other.
Three rules §
A beacon follows three rules:
- A blink takes 120 steps, from dark up to full brightness and back down.
- Each segment's beacons start their blinks at a different point from the last segment's, so the towers along the road blink at different times.
- The tower halfway along a segment blinks exactly opposite the tower at the segment's end.
Every rule reads the tower and the clock alone. So the ride derives each beacon afresh, every frame. Ask for a beacon at step 5,430 cold, and the answer comes straight from the clock.
Which beacon, and when §
A tower's sighting asks for its beacon with one line. Here it is for the tower past a segment's end:
# ...
beacon = Beacon.at({ segment: in_view.index, place: AtEnd }, clock)
# ...
The record names the beacon. in_view.index is the segment's index in the route, counting from 0, and AtEnd is the tower's place on it. The tower halfway along passes Midway instead. The record's type is a TowerKey:
## Where on its segment a tower stands: past the segment's end, or halfway
## along it.
TowerPlace : [AtEnd, Midway]
## Which tower a beacon tops: the segment it stands on, and its place
## there. Towers aren't kept, so this pair is all that tells one from
## another.
TowerKey : { segment : Route.SegmentIndex, place : Beacon.TowerPlace }
The clock says when. Here's the function the line calls:
## The beacon of the tower at `key`, as it shines at `clock`.
at : Beacon.TowerKey, Clock -> Beacon
at = |key, clock| Beacon.(beacon_brightness(Beacon.blink_phase(key, clock)))
A Beacon is an opaque type around a Fraction, its brightness, and Beacon.(…) builds one. Only the Beacon module can build one, so every beacon in the ride comes from this function.
The phase §
The brightness comes from the beacon's phase, which counts how many steps into its blink the beacon is:
## The phase at `clock` of the beacon of the tower at `key`: how many steps
## into its blink it is, from 0, where it starts dark, up to a step short of
## one `beacon_period`. It is the steps since the ride's start, plus the
## beacon's head start, wrapped to one blink.
blink_phase : Beacon.TowerKey, Clock -> Steps
blink_phase = |key, clock|
into_blink(clock.steps_since(Clock.start).plus(head_start(key)))
clock.steps_since(Clock.start) turns a moment into a length: the steps since the ride began. The beacon's head start goes on top, and that's where the second and third rules live:
## How far into its blink the beacon of the tower at `place` on the segment at
## `segment` is when the ride starts. Each segment's beacon starts a stagger
## further into its blink than the last one's, so neighbouring segments'
## beacons blink apart, and the tower halfway along starts half a blink on
## from the one at the end, so the two blink opposite each other.
head_start : Beacon.TowerKey -> Steps
head_start = |{ segment, place }| {
at_end = into_blink(beacon_stagger.times(segment.to_u32()))
match place {
AtEnd => at_end
Midway => into_blink(at_end.plus(half_period))
}
}
beacon_stagger is 37 steps, so each segment's beacon starts 37 steps further into its blink than the last one's. 37 and 120 have only 1 as a common factor, so the first 120 segments' beacons all start at different points of the blink. The tower halfway along starts half a blink later than the tower at the end, so the two blink opposite each other. half_period is the period divided by two, so it follows the period if the period changes.
into_blink brings a sum of steps inside one blink:
## `steps` brought inside one blink: so many steps into a blink, from 0 up to a
## step short of one `beacon_period`.
into_blink : Steps -> Steps
into_blink = |steps| steps.wrapped_to(Beacon.beacon_period)
wrapped_to keeps the remainder after dividing by the period. So the route's second segment, at index 1, has a beacon at its end with a head start of 37 steps. At step 83 the sum is 120, a whole blink, and its phase is back to 0.
The brightness §
A cosine turns the phase into a brightness:
## How bright a beacon is `phase` steps into its blink.
beacon_brightness : Steps -> Fraction
beacon_brightness = |phase| {
angle = 2.0 * F64.pi * phase.to_f64() / Beacon.beacon_period.to_f64()
Fraction.of((1.0 - angle.cos()) / 2.0)
}
(1 - cos) / 2 is 0 at the start of a blink, 1 halfway through, and 0 again at the end. A cosine rises and falls smoothly, so the beacon eases into full brightness and out of it again. The formula also stays between 0 and 1 all the way. That matters, because Fraction.of enforces the invariant that every share lies between 0 and 1, with a crash.
The picture's two beacons belong to the first segment, whose head start is 0. Step 5,400 is 45 whole blinks into the ride, so the beacon at the end is dark there, and the one halfway along is at full brightness. A quarter of a blink later, at step 5,430, the two are equally bright. By step 5,460 they've changed places.
A disc, or a refusal §
Tower.tower_shapes passes Beacon.disc two things: the point on the screen where the beacon's middle lands, and how many pixels a metre spans there. disc returns a disc, or a refusal:
disc : Beacon,
{
center : Point,
metre_on_screen : Pixels,
} -> Try(Shapes.Shape, [TooDim, TooSmall])
The disc is 3 metres in radius, and see-through as far as the beacon is dim. disc returns TooDim for a beacon below a brightness of one part in fifty, and TooSmall for a disc whose radius comes to under half a pixel. Every beacon bright enough and big enough gets its disc.
Everything Beacon works with comes in as arguments: a tower's key, a clock, a point on the screen and a scale. So its tests build those values directly, apart from any tower or ride. They check the head starts, the phase, the blink and the opposite blink halfway along, and the disc's size, color and refusals.
A beacon repeats every 120 steps. The sun moves one way through the whole ride, and the sky and the light on everything follow it.