Chapter 30
The rider's gaze
The herds chapter left one pig waiting. On the first two segments with pigs, one pig in the big herd catches the rider's eye. The rider turns to look and slows down, and the camera turns too.
Looking and slowing down last different lengths of time. The eyes give up once the pig is too far to the side, and the speed stays down until the segment ends. So the code has two words for them: a look is what the eyes do, and gawking is what the speed does. This chapter takes them in that order.
speed, gaze_yaw and look_depth.Get the types right §
Two fields of the rider's record carry the gaze:
| Field | Type | What it is |
|---|---|---|
gaze_ |
Angle. |
How far the rider looks from the way the bike points |
look_ |
Fraction |
How far a look went, from 0 to 1, fading once the gaze is back on the road |
Each angle is a Turn from the one before: yaw is how far the bike points from the road, and gaze_yaw is how far the eyes point from the bike.
Three small types say what a pig can do to the rider. A segment's pigs field is NoPigs, or a Herd with its interest, which says whether the herd catches the rider's eye:
## A segment's herd of pigs, if it has one, and how the rider takes it.
Pigs : [NoPigs, Herd({ herd : PigHerd, interest : Segment.PigInterest })]
The rider looks at a pig or at the road, and only a pig has an angle:
## Where the rider wants to look: at the pig in sight, this far from the
## way the motorcycle points, or at the road.
GazeTarget : [OnPig(Angle.Turn), OnRoad]
So the job is a short list:
| Job | Function | Its type |
|---|---|---|
| Choose where to look | desired_ |
Motorcycle, Segment -> Gaze. |
| Turn the head | next_ |
RiderEyes, Gaze. |
| Keep the depth of the look | next_ |
RiderEyes, Angle. |
| Tell whether the rider has noticed a pig | noticed_ |
Motorcycle, Segment -> [Noticed(Geom. |
| Work out the brake | gawk_ |
Motorcycle, Segment -> ThrottleLimit |
The chain from desired_gaze is the look, and gawk_brake is the gawking.
The look §
Rider.rider_at_next_step moves the eyes last, after the bike has moved:
## These eyes moved on a step toward `target`: the gaze, and the look depth
## it leaves.
eyes_at_next_step : RiderEyes, Gaze.GazeTarget -> RiderEyes
eyes_at_next_step = |eyes, target| {
gaze_yaw = next_gaze_yaw(eyes, target)
{ gaze_yaw, look_depth: next_look_depth(eyes, gaze_yaw) }
}
Before the eyes move, the rider chooses where to look. RiderMind.gaze_target hands that question to Gaze.desired_gaze:
## Where the rider wants to look: at a distracting pig that is near enough
## to notice and less than the release angle from the way the motorcycle
## points, or else at the road.
desired_gaze : Motorcycle, Segment -> Gaze.GazeTarget
desired_gaze = |motorcycle, segment|
match Gaze.noticed_pig(motorcycle, segment) {
Noticed(pig) => {
rider_point = motorcycle.route_place.road_point
turn = turn_to({ rider_point, yaw: motorcycle.yaw }, pig)
if turn.abs() < look_release_angle {
OnPig(turn)
} else {
OnRoad
}
}
NothingNoticed => OnRoad
}
The pig has to be near, and it has to be in sight. Near means from 150 metres before the pig, measured along the road. In sight is an angle:
## How far `pig` stands from the way a motorcycle at `rider_point` points, its
## `yaw`.
turn_to : { rider_point : Geom.RoadPoint, yaw : Angle.Turn }, Geom.Placed(Billboard) -> Angle.Turn
turn_to = |{ rider_point, yaw }, pig|
Angle.Turn.radians(
F64.atan2({
x: pig.road_point.along.distance_from(rider_point.along).to_metres(),
y: pig.road_point.across.distance_from(rider_point.across).to_metres(),
}),
).minus(yaw)
turn_to is the direction to the pig, less the way the bike points. The pig stands beside the road, so the angle grows as the rider comes closer, and at 35 degrees the rider lets it go.
Turning the head §
## The gaze one step on. It swivels toward a pig by at most the swivel rate.
## With only the road to look at, it turns back by the return rate, and the
## last step lands it straight ahead.
next_gaze_yaw : RiderEyes, Gaze.GazeTarget -> Angle.Turn
next_gaze_yaw = |eyes, target| {
gaze = eyes.gaze_yaw
match target {
OnPig(turn) => gaze.plus(turn.minus(gaze).clamped_within(look_swivel_rate))
OnRoad => gaze.minus(gaze.clamped_within(look_return_rate))
}
}
Toward a pig, the gaze turns by at most 4 degrees a step, and on the way back by at most 0.2, held there by clamped_within. A gaze within 0.2 degrees of straight ahead goes all the way home, so the return ends exactly there.
The bike keeps its own heading. The gaze goes to the camera alone:
## How far the rider's head turns from the way the motorcycle points: the
## gaze, and a little more with the lean, as a rider's head turns into a bend.
head_yaw : Rider -> Angle.Turn
head_yaw = |rider| rider.eyes.gaze_yaw.plus(rider.motorcycle.lean.steer(head_turn_per_lean))
The second term is the rider's head going with the lean. When the rider looks right, the road slides left on the page, and the bike keeps going where it was going.
The depth of a look §
## The rider's look depth after the gaze moves to `gaze_yaw`. It rises with
## how far the gaze has turned, and it holds while the gaze is off straight
## ahead by any amount, so it holds all through the return. It fades only
## once the return has put the gaze exactly straight ahead.
next_look_depth : RiderEyes, Angle.Turn -> Fraction
next_look_depth = |eyes, gaze_yaw|
if gaze_yaw == Angle.straight_ahead {
Fraction.clamp(eyes.look_depth.to_f64() - look_depth_decay.to_f64())
} else {
eyes.look_depth.max(Fraction.clamp(gaze_yaw.abs().ratio_to(Gaze.look_release_angle)))
}
While the gaze is off straight ahead, the depth rises with how far it has turned, and holds there. Once the gaze is home, the depth fades by 0.0012 a step, so a full look takes more than 800 steps to fade.
The depth has one job. It widens the view:
## How strongly the depth of these eyes' look holds the rider's eye. The
## easing keeps a shallow look from widening the view much.
attention : RiderEyes -> Attention
attention = |eyes| Attention.from_fraction(Easing.smoothstep(eyes.look_depth))
The Attention type, an opaque F64, is how hard something holds the rider's eye, with a full look at 1. Easing.smoothstep eases the look's pull in and out. RiderCamera.ride_focal_length sets how wide the view is from Rider.attention, the stronger of this look's pull and a crossing cat's. So a pig widens the view, and the view stays wide for a while after.
Gawking §
Gawking asks a plainer question: has the rider noticed a pig on this segment?
## The pig the rider has noticed on this segment, if any. The noticing
## starts where an eye-catching pig is `notice_distance` ahead and asks only
## where the motorcycle is, so it lasts past the pig to the end of the
## segment.
noticed_pig : Motorcycle, Segment -> [Noticed(Geom.Placed(Billboard)), NothingNoticed]
noticed_pig = |motorcycle, segment| {
rider_along = motorcycle.route_place.road_point.along
match eye_catching_pig(segment) {
CatchesEye(pig) if is_past_notice(rider_along, pig) => Noticed(pig)
CatchesEye(_) | NoPigCatchesEye => NothingNoticed
}
}
The if after CatchesEye(pig) is a guard: the branch matches only when the tag matches and the guard is true. Like the look, the noticing starts when the pig is within 150 metres. It asks only where the rider is, so it lasts until the segment ends, long after the look.
gawk_brake is the third link in the throttle's chain, a brake while the rider has noticed a pig:
# ...
Gawking.gawk_brake(motorcycle, segment),
# ...
## The brake for a gawking rider, who aims to be down to `gawk_speed` by
## the settle distance before the pig. The noticing lasts to the end of
## the segment, so the brake holds the rider down until the next one.
gawk_brake : Motorcycle, Segment -> ThrottleLimit
gawk_brake = |motorcycle, segment|
match Gaze.noticed_pig(motorcycle, segment) {
Noticed(pig) => {
gap = pig.road_point.along.distance_from(motorcycle.route_place.road_point.along)
room = gap.minus(gawk_settle_distance)
AtMost(
VehicleLimits.braking_to(
motorcycle.speed,
{ target: gawk_speed, within: room.max(gawk_least_distance) },
),
)
}
NothingNoticed => NoLimit
}
The formula is the throttle's bend brake, with a pig for a bend: the rider aims to be down to 0.2 metres a step by 25 metres before the pig. Past that point, gawk_least_distance holds room at a metre, so the brake stays on.
The first pig §
Here's the look and the gawking on the third segment of the route:
| Step | What happens | Speed | gaze_ |
look_ |
|---|---|---|---|---|
| 885 | The rider looks, with the pig just inside 150 metres | 1.94 metres a step | 4.0 degrees | 0.114 |
| 1,049 | The gaze is at its widest | 0.2 metres a step | 34.95 degrees | 0.999 |
| 1,050 | The rider lets the pig go | 0.2 metres a step | 34.75 degrees | 0.999 |
| 1,224 | The gaze is back on the road | 0.2 metres a step | 0 degrees | 0.997 |
The look takes 165 steps, and getting the eyes back takes 174. The rider crawls on, eyes on the road, to the next segment at step 1,397. An expect holds that the gaze is home at every one of the ride's 18 seams between segments.
That's the pig's part of the rider's step. The truck is the other stepped thing, and its chapters start with its sprite.