Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

Chapter 34

The bird

A bird sits on a treetop by the road. As the rider comes near, it takes off, flies across the road and ahead, and lands on a tree on the far side. The bird is Roc's logo, and it moves the way the cat does: one plan, a stage from the clock, and a timeline of frames.

Press play and watch the bird fly.

The picture's camera stays put, 18 metres short of the first tree, and the gap counts down as if a rider were coming at 1.25 metres a step. The bird leaves its perch, a treetop 6.4 metres up, at a gap of 60 metres. It rises to 10.7 metres up in the middle of its flight, and lands on a treetop 4.9 metres up at 15.

The same plan §

The ride plans a flight as it plans a crossing: once, when the rider first comes within reach. A flight is 36 frames, and flight_span sets it to end when a rider keeping their speed would be 15 metres short of the tree the bird left. So the rider sees the bird land before passing that tree:

BirdFlight.roc
## What the ride plans a flight by: its length, and the gap from the rider
## to the tree it left at which it has landed.
flight_span : EncounterPlan.StepSpan
flight_span = { length: Steps.of_count(flight_frames.len()), ends_at: landing_gap }

A faster rider sets the bird off farther away:

Rider's speed The flight starts at a gap of
1 metre a step 51 metres
1.25 metres a step 60 metres
2 metres a step 87 metres

Each segment holds one encounter at most: a cat crossing, a bird's flight, or a quiet road. One function finds the stage of whichever it is, with that animal's own length:

Encounter.roc
## Where `encounter`'s animation, started by `plan`, has got to with the
## ride's clock at `now`, as long as its span lasts. A quiet road has nothing
## to wait for, so it stays `Waiting`.
encounter_stage : Encounter, { plan : EncounterPlan, now : Clock } -> Animation.Stage
encounter_stage = |encounter, { plan, now }|
	match encounter.encounter_span() {
		Ok({ span, .. }) => plan.stage_at({ now, length: span.length })
		Err(QuietRoad) => Waiting
	}

encounter_span returns the cat's span or the bird's, and { span, .. } takes its span field. A quiet road stays Waiting for good.

Its poses §

A bird's pose can carry a tag of its own. Side on or seen from behind, its wings are up or down, so those two tags carry a WingBeat:

BirdFlight.roc
## Where the wings are in a beat.
WingBeat : [WingsUp, WingsDown]
BirdFlight.roc
## How the bird looks: sitting; side on as it takes off; seen from behind
## as it flies off; and dropping onto its tree.
BirdPose : [
	Perched,
	Side(BirdFlight.WingBeat),
	Away(BirdFlight.WingBeat),
	Diving,
]

A flight's 36 frames come in three runs:

Frames What the bird does
1 to 4 one flap, side on
5 to 32 seven flaps, seen from behind
33 to 36 dives

Before the first frame the bird sits on the tree it leaves, and after the last it sits on the tree it lands on. The poses are one list, built from the three runs:

BirdFlight.roc
## How the bird looks through a flight, a frame a step: a flap side on as it
## takes off, seven flaps seen from behind, and a flap's length of diving. The
## seven is chosen by eye.
flight_poses : List(BirdFlight.BirdPose)
flight_poses = [
	flap_poses({ up: Side(WingsUp), down: Side(WingsDown) }),
	List.repeat(flap_poses({ up: Away(WingsUp), down: Away(WingsDown) }), 7).join(),
	List.repeat(Diving, 2 * frames_a_half_flap),
].join()

Each frame is a pose, and how far through the flight it is:

BirdFlight.roc
## One frame of any flight before it is put between two trees: how the bird
## looks, and how far through the flight it is.
FlightFrame : { pose : BirdFlight.BirdPose, through : Fraction }
BirdFlight.roc
## The frames of any flight, one a step: `flight_poses`, each as far through
## the flight as its place in the list, from the start at the first to the end
## at the last. Both the flight's length and its timeline come from this one
## list, so the two can't disagree.
flight_frames : List(FlightFrame)
flight_frames = {
	last_frame = (flight_poses.len() - 1).to_f64()
	flight_poses.map_with_index(
		|pose, frame| { pose, through: Fraction.of(frame.to_f64() / last_frame) },
	)
}

The first frame is 0 of the way through, and the last is all of it. map_with_index passes each pose along with its place in the list, and that place over the last frame's place is the frame's share of the way.

Its path §

A BirdFlight is a record of two trees, from and to. Its timeline is the same Animation.Timeline as the cat's, with a FlightFrame for each frame and a BirdPose for each pose:

BirdFlight.roc
## The whole of a flight: `flight_frames`, on one smooth path that rises in
## the middle, from the tip of the tree it leaves to the tip of the one it
## lands on.
flight_timeline : BirdFlight -> Animation.Timeline(FlightFrame, BirdFlight.BirdPose)
flight_timeline = |{ from, to }| {
	ends = { from: tree_top(from), to: tree_top(to) }
	Animation.Timeline.of({
		before: sitting_on(from),
		frames: flight_frames,
		frame_instant: |{ pose, through }| {
			pose,
			location: flight_location({ from: ends.from, to: ends.to, through }),
		},
		after: sitting_on(to),
	})
}

The path runs between the two treetops, eased at each end, and rises in an arc 5 metres high at the middle:

BirdFlight.roc
## The location `through` a flight between `from` and `to`, easing out and in along
## a straight line, and raised by an arc over it.
flight_location : {
	from : Animation.Location,
	to : Animation.Location,
	through : Fraction,
} -> Animation.Location
flight_location = |{ from, to, through }| {
	on_line = Animation.location_between({ from, to, fraction: Easing.smoothstep(through) })
	share = through.to_f64()
	# 4t(1 - t) is 0 at both ends and 1 halfway, so the arc tops out at `flight_arc`.
	above_line = flight_arc.times(4.0 * share * (1.0 - share))
	{ ..on_line, lift: on_line.lift.plus(above_line) }
}

Easing.smoothstep turns a share of the way into one that moves slowly at each end and faster in the middle, so the bird eases away from one tree and into the next. For t, the share of the way through, 4t(1 − t) is 0 at both ends and 1 halfway, so the arc starts and ends at the treetops. The last line is a record update: the point on the line, with its lift raised.

Finding its trees §

A segment gets its bird once, when the world is built. The bird perches on the fifth tree on the left, and flies to the first tree on the far side at least 25 metres farther along:

BirdFlight.roc
## The flight of a bird perched on the tree at `perch`, among a segment's
## `trees`: to the first tree on the other side of the road far enough along.
flight_from : List(Trees.RoadsideTree), Trees.RoadsideTree -> Try(BirdFlight, [NoTreeToFlyTo])
flight_from = |trees, perch| {
	is_far_enough = |tree| tree.road_point.along >= perch.road_point.along.plus(shortest_hop)
	is_landing_tree = |tree| is_far_side(tree) and is_far_enough(tree)
	to = trees.find_first(is_landing_tree) ? |NotFound| NoTreeToFlyTo
	Ok(BirdFlight.({ from: perch, to }))
}

find_first returns a Try. The ? takes the tree it found, or returns the failure from flight_from, renamed to the bird's own, NoTreeToFlyTo, as in the “Errors as tag unions” chapter. The bird always starts on the left because its perched and side-on stills face right, so on a left tree it faces the road.

The segment puts the perch and the flight together:

Segment.roc, in from_choices
# ...
BirdHere => {
	perch =
		fifth_left_tree(trees)
			?? crash "Segment.from_choices: a segment planned with a bird has a tree for it to start on"
	flight =
		BirdFlight.flight_from(trees, perch)
			?? crash "Segment.from_choices: a segment planned with a bird has a tree for it to fly to"
	BirdHere(flight)
	# ...

The route's plan for the segment says BirdHere, a bare tag, and the segment's own encounter holds BirdHere(flight), the flight itself. The route plans a bird only on a segment with both trees, so every bird's segment holds both: an invariant. ?? opens a Try another way: it takes the value from an Ok, and for an Err it runs the expression on its right instead. Here that's a crash, which enforces the invariant with its own message for each tree.

Into the frame §

The bird reaches the frame as the cat does, with a Bird as its kind. The rider's own segment plays its flight, and a bird on a segment ahead sits on its perch. A perched bird stands exactly at its tree's top, at the same depth as the tree. The scene paints trees before birds when depths tie, so the bird shows on top of its tree.

The bird is there to be seen. Its whole cost to the ride is a list of poses, a path, and a still for each pose. A cat costs more, because the rider reacts to it.