Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

Chapter 33

The cat

A cat waits behind a tree by the road. As the rider comes near, it walks out into the lane, freezes, and leaps clear just before the rider arrives. The cat is the most elaborate animation in Safari Ride, and it's where the rider and the world start to change each other.

Press play and watch the crossing.

The picture brings a rider in at a metre a step, from 50 metres off. The cat waits until the gap is 41 metres, walks out, and freezes in the lane at 31. It coils at 7, springs just under 6, and lies collapsed on the far verge by 3.

One step carried §

A crossing is a list of frames, and it plays one frame a step on the ride's clock. So the ride has to carry only one fact about it, the step it starts on. That's an EncounterPlan:

EncounterPlan.roc, in EncounterPlan
## When the encounter on the rider's segment starts: not planned yet, or at a
## step of the ride's clock. The ride keeps one, and forgets it at each new
## segment, since only the encounter on the rider's own segment is ever live.
EncounterPlan := [Unplanned, StartsAt(Clock)].{
# ...

The ride derives everything else about the cat in a frame from the plan and the clock:

EncounterPlan and the clock Animation.Stage where the crossing has got to  stage_at Animation.Instant the cat's pose and location  the timeline's instant Geom.Placed a cat on the road  EncounterPoses places it a sighted cat in the frame  EncounterSighting sights it
How a cat reaches the frame

When it starts §

The ride plans the crossing once, from how fast the rider is coming. The cat should be across just as the rider comes within 3 metres of it. What the ride plans by is a span: how many steps the animation takes, and the gap it ends at.

EncounterPlan.roc
## How many steps an animation plays for, a frame a step, and the gap from
## the rider to the animal it ends at.
StepSpan : { length : Steps, ends_at : Distance }

The cat's span is its frames, ending 3 metres short:

CatCrossing.roc
## What the ride plans a crossing by: its length, and the gap it ends at.
crossing_span : EncounterPlan.StepSpan
crossing_span = { length: Steps.of_count(crossing_frames.len()), ends_at: clear_gap }

The crossing has 38 frames. So the ride makes the plan on the first step where a rider keeping their speed would cover the rest of the gap, down to 3 metres, within 38 steps. A faster rider covers more road in that time, so the cat sets off farther away:

Rider's speed The crossing starts at a gap of
1 metre a step 41 metres
1.5 metres a step 60 metres
2 metres a step 79 metres
EncounterPlan.roc
## The start of an animation that is to end as the rider comes within
## `ends_at`, planned at `now` from how the rider comes then: `gap` from the
## animal, at `speed`. It starts now on the first step that a rider keeping
## their speed would arrive within its length, so a faster rider has it
## start farther off. Till then, and while the rider is at rest, it isn't
## planned. The plan looks at the rider's speed that once: after it, the
## animal doesn't care what the rider does.
##
## Planning on the step itself keeps the guess short, an animation's length
## ahead. A plan from farther off would take the speed to be steady for
## longer, and a rider speeding up meanwhile would arrive before the end.
plan_for : EncounterPlan.StepSpan,
{
	gap : Distance,
	speed : Speed,
	now : Clock,
} -> EncounterPlan
plan_for = |span, { gap, speed, now }|
	if speed == Speed.at_rest {
		Unplanned
	} else if gap <= EncounterPlan.planning_gap(span, speed) {
		StartsAt(now)
	} else {
		Unplanned
	}

The plan reads the rider's speed on that one step. After that, the cat plays on the clock alone. A rider who brakes arrives a little late, and the cat has crossed on time.

Where it has got to §

The plan and the clock say where the crossing has got to, as a Stage:

Animation.roc
## Where an animation has got to: not begun, playing and this many steps in,
## which is the frame it shows, or over this many steps ago.
Stage : [Waiting, Playing(Steps), Past(Steps)]

Waiting comes before the crossing starts. Playing counts the steps in, and Past counts the steps since it ended. One function works the stage out:

EncounterPlan.roc
## Where an animation `length` steps long, started by `plan`, has got to with
## the ride's clock at `now`. An unplanned animation is still waiting.
stage_at : EncounterPlan, { now : Clock, length : Steps } -> Animation.Stage
stage_at = |plan, { now, length }|
	match plan {
		Unplanned => Waiting
		StartsAt(first_step) => {
			end = first_step.plus(length)
			if now < first_step {
				Waiting
			} else if now < end {
				Playing(now.steps_since(first_step))
			} else {
				Past(now.steps_since(end))
			}
		}
	}

What it looks like §

The crossing's frames come in three runs, one frame a step:

Frames What the cat does
1 to 10 walks out, striding and resting, from behind its tree toward the middle of the road
11 to 34 stands frozen in the lane
35 to 38 coils, flies for two frames, and lands on the far verge

The table counts frames from 1, as the picture does. A Playing stage counts its steps from 0. A cat holds itself in one of seven poses:

CatCrossing.roc
## How the cat holds itself: walking, it is at rest or mid-stride; then
## frozen; then its leap, from the coil through flight and landing to the
## collapse after.
CatPose : [Rest, Stride, Frozen, Coil, Flight, Land, Collapse]

Each frame is a pose, and where the cat is on its way across:

CatCrossing.roc
## One frame of any crossing before it is put on a road: how the cat looks, and
## where it is on its way across.
CrossingFrame : { pose : CatCrossing.CatPose, way : CrossingWay }
CatCrossing.roc
## Where a cat is on its way across in one frame: walking out, this much of
## the way to the middle of the road; standing frozen there; or leaping, this
## much of the way on to the far verge and this high.
CrossingWay : [
	WalkingOut(Fraction),
	StandingFrozen,
	Leaping({ way_to_verge : Fraction, lift : Distance }),
]

A frame holds how far along its way the cat is, as a share of the way. The road comes in later, by the cat's own tree, so one list of frames serves every crossing. The leap is four of them, set by eye:

CatCrossing.roc
## The leap, frame by frame: how the cat looks, how much of the way from the
## middle of the road to the far verge it has come, and how high its feet are.
## These are keyframes set by eye, not measured off the stills, and each shows
## for one step.
leap_frames : List(CrossingFrame)
leap_frames = [
	{ pose: Coil, way: Leaping({ way_to_verge: Fraction.zero, lift: Distance.zero }) },
	{ pose: Flight, way: Leaping({ way_to_verge: Fraction.of(0.4), lift: Distance.metres(0.27) }) },
	{
		pose: Flight,
		way: Leaping({ way_to_verge: Fraction.of(0.85), lift: Distance.metres(0.17) }),
	},
	{ pose: Land, way: Leaping({ way_to_verge: Fraction.whole, lift: Distance.zero }) },
]

One list joins the three runs:

CatCrossing.roc
## The frames of any crossing, one a step. Both the crossing's length and its
## timeline come from this one list, so the two can't disagree.
crossing_frames : List(CrossingFrame)
crossing_frames = {
	walking = walking_poses.map_with_index(
		|pose, frame| {
			pose,
			way: WalkingOut(Fraction.of(frame.to_f64() / walking_poses.len().to_f64())),
		},
	)
	frozen = List.repeat({ pose: Frozen, way: StandingFrozen }, frozen_frames)
	[walking, frozen, leap_frames].join()
}

map_with_index hands each walking pose its place in the list, which gives its share of the way out. List.repeat makes the 24 frozen frames.

A timeline §

A list of frames and a way to show each one make a timeline. Animation defines it once, for any animal:

Animation.roc, in Timeline
# ...
Timeline(frame, pose) :: {
	before : Animation.Instant(pose),
	frames : List(frame),
	frame_instant : frame -> Animation.Instant(pose),
	after : Animation.Instant(pose),
}.{
# ...

For the cat, a frame is a CrossingFrame and a pose is a CatPose. before and after are the animal at rest, outside the frames. An Instant(pose) is a record of the pose and the animal's location.

crossing_timeline puts a crossing's frames on the road, by its own tree:

CatCrossing.roc
## The whole of a crossing, a frame a step: the cat walks out to the middle
## of the road, freezes there, and leaps clear to the far verge.
crossing_timeline : CatCrossing -> Animation.Timeline(CrossingFrame, CatCrossing.CatPose)
crossing_timeline = |crossing| {
	on_road = |across| { road_point: { along: crossing.along(), across }, lift: Distance.zero }
	start = on_road(crossing.start_across())
	middle = on_road(CatCrossing.mid_across)
	end = on_road(CatCrossing.end_across)
	location_of = |way|
		match way {
			WalkingOut(fraction) =>
				Animation.location_between({ from: start, to: middle, fraction })
			StandingFrozen => middle
			Leaping({ way_to_verge, lift }) => {
				on_the_way = Animation.location_between({
					from: middle,
					to: end,
					fraction: way_to_verge,
				})
				{ ..on_the_way, lift }
			}
		}
	Animation.Timeline.of({
		before: { pose: Rest, location: start },
		frames: crossing_frames,
		frame_instant: |{ pose, way }| { pose, location: location_of(way) },
		after: { pose: Collapse, location: end },
	})
}

location_of is a function defined inside another, so it can use the crossing's start, middle and end. Animation.location_between returns the location a fraction of the way from one location to another. Asking a timeline for the animal at a stage is one match:

Animation.roc, in Timeline
# ...
instant : Animation.Timeline(frame, pose), Animation.Stage -> Animation.Instant(pose)
instant = |Animation.Timeline.(timeline), stage|
	match stage {
		Waiting => timeline.before
		Playing(steps_in) =>
			match timeline.frames.get(steps_in.to_u64()) {
				Ok(frame) => (timeline.frame_instant)(frame)
				Err(OutOfBounds) =>
					crash "Animation.Timeline.instant: a stage plays past the last frame, so the span it was staged by disagrees with its timeline"
			}
		Past(_) => timeline.after
		# ...

frame_instant is a function stored in a record's field, so calling it takes parentheses: (timeline.frame_instant)(frame). Written timeline.frame_instant(frame), it would read as a method call. The invariant is that the span and the frames agree, so instant crashes on a Playing stage past the last frame. Both come from the one list, crossing_frames, so every Playing stage finds its frame.

Into the frame §

The ride plays only the crossing on the rider's own segment. A cat on a segment ahead waits behind its tree:

EncounterPoses.roc
## The cats on `road`, with the encounter on the rider's segment at
## `encounter_stage`, in the order of `RoadAhead.segments_in_view`.
cat_poses : RoadAhead.RoadView, Animation.Stage -> List(EncounterPoses.PlacedAnimal(Cat))
cat_poses = |road, encounter_stage|
	RoadAhead.segments_in_view(road).keep_oks(
		|in_view| segment_cat({ in_view, encounter_stage }),
	)

A Cat is the cat's kind in the frame: a record of its pose, with the methods every kind has. A PlacedAnimal(Cat) is a Geom.Placed cat, with the segment it stands on. EncounterSighting then sights each cat through Pipeline.scenery_sighting_of, as the pipeline sights a tree. From there, a cat joins the frame's depth sort like any other thing.

The ride carries one step for the cat, and derives the rest, frame by frame, from that step and the clock. While it crosses, the cat also asks two things of the rider: hold the throttle, and look. After the bird's chapter, the “What a cat does to the rider” chapter takes the rider's side.