Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

Chapter 16

The road

In front of the mountains, the road comes nearer as the rider rides. It's ground, laid on the grass in front of the mountains and under everything beside the road.

Step 1,529 of the ride: the road, over the grass and under the trees.

The ground comes in patches, and a patch is a list of corners on the ground and a color. The road's patches all come from one function:

GroundSighting.roc
## A patch of road with these corners.
road_patch : List(RiderView.RiderPoint) -> GroundPatch
road_patch = |corners| GroundPatch.of(corners, road_color)

Each corner is a RiderPoint, a point on the ground seen from the rider, so the GroundPatch module can find where each corner lands on the screen and place the patch there. The “The math layer” chapter follows a corner onto the screen.

A segment's ground §

The route is a chain of straight segments with a bend between each one and the next. Each segment in view gets its own ground from one function:

GroundSighting.roc, in ground_patches
# ...
ground_patches : RoadAhead.RoadView,
{
	vantage : Geom.Vantage,
	in_view : RoadAhead.SegmentInView,
} -> List(GroundPatch)
ground_patches = |road, { vantage, in_view }| {
	along_segment =
		match in_view.from_rider {
			Ahead(_) => [road_along(road, { vantage, in_view })]
			JustBehind => []
		}
	bend =
		match BendInView.bend_after(road, in_view) {
			Ok(bend_in_view) => [bend_paving(road, { vantage, bend_in_view })]
			Err(RoadEndsInView) => []
		}
	[
		along_segment,
		bend,
		pond_patches(road, { vantage, in_view }),
	].join()
	# ...

A segment's ground is up to three things: the road along it, the paving at its bend into the next segment, and a pond if it ends at one. Each comes as a list, and join makes them one. The segment just behind the rider can still show its last stretch, and its bend's paving lays that stretch, so its own road list is empty. The last segment of the road ahead gets an empty list for its bend, RoadEndsInView, since the road in view ends there.

Along a segment §

The road along a segment is one patch:

GroundSighting.roc
## The road along a segment in view, as one patch: across its start, up its
## right edge to the end of each chunk, then back down its left edge. It is
## one patch, not a patch a chunk, because a canvas smooths each patch's
## edges on its own, and where two patches meet the grass under them shows
## through as a pale seam.
road_along : RoadAhead.RoadView,
{
	vantage : Geom.Vantage,
	in_view : RoadAhead.SegmentInView,
} -> GroundPatch
road_along = |road, { vantage, in_view }| {
	from_rider = in_view.from_rider
	segment_length = in_view.segment.length
	segment_chunk_count = chunk_count(segment_length)
	corner = |chunk_number, across| {
		along = chunk_end(segment_length, chunk_number)
		RoadAhead.rider_point_at(road, { vantage, from_rider, road_point: { along, across } })
	}
	up_right_edge =
		List.from_iter(U32.until(0, segment_chunk_count + 1))
			.map(|number| corner(number, Geom.right_edge))
	down_left_edge =
		List.from_iter(U32.until(0, segment_chunk_count))
			.map(|k| corner(segment_chunk_count - k, Geom.left_edge))
	road_patch([corner(0, Geom.left_edge)].concat(up_right_edge).concat(down_left_edge))
}

corner is a function named inside the body. It uses road, vantage and from_rider from around it, so each call names only a chunk and an edge. A function that uses names from around it like this is a closure.

The patch starts at the left corner of the segment's start, runs up the right edge to the end of every chunk, and comes back down the left, so its corners go round in order. So the right edge counts one corner more than the left: the left's first corner is already the patch's first. U32.until(0, n) counts from 0 up to just short of n, and List.from_iter collects the count into a list of chunk numbers.

The edges are cut into chunks of at most road_chunk_length, 25 metres. The ride's ground curves away from the rider, on its 100 km radius, and a straight edge any longer would part from the curve by more than a pixel near the horizon.

All of a segment's chunks share one patch. The platform smooths each patch's edges on its own, and where two patches met, the grass would show through as a pale seam.

At a bend §

Where one segment bends into the next, the two roads leave a gap on the outside of the turn. Here are three bends, seen from above:

Three bends from above. The off-white dot is where the view is from, the orange lines are the guard rails, and the yellow dot is each bend's apex, where the two roads' outer edges meet.

The paving fills the bend's outer corner. The BendInView module works it out, one step at a time:

Job Function Its type
Find the outer edge outer_edge BendInView -> Across
Find the corner, on the road outer_corner BendInView -> BendInView.OuterCorner(Geom.RoadPoint)
See the corner from the rider outer_corner_in_view BendInView, { road : RoadAhead.RoadView, vantage : Geom.Vantage, } -> BendInView.OuterCorner(RiderView.RiderPoint)

A bend to the right has its outer edge on the left, so the paving changes sides as the road turns. An outer corner is three points: where the first road's outer edge ends, where the next road's outer edge starts, and between them the apex, where the two outer edges, carried on straight, cross. OuterCorner takes the kind of point as a type variable, so the same record holds road points first and rider points after.

GroundSighting.bend_paving lays the corner as one more patch. It starts 40 metres before the bend, lead_in_length, runs up the left edge, across the end of the next road's first chunk, and back down the right. Whichever side is outside the turn takes in the corner's three points. Both roads end inside the paving, so road lies over road where they meet, and the patches join cleanly. Since the paving lays the last 40 metres of the road before the bend, it stands in for the road along the segment just behind. Forty metres is enough, because only a bend sharper than a right angle would show road further back, and every bend on the route is gentler than that.

The ponds are ground too. A segment that ends at a pond lays two more patches, its bank and then its water.

The corner has its paving. Next, each bend gets a rail around its outside.