Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

Chapter 10

Lists and map: a far tree

With a triangle and a rectangle in hand, we can build the far tree. The tree comes from small functions: the tiers of its crown, a test that pins them, a triangle for each tier, and the order of the trunk and the tiers in its sprite.

One far tree, magnified so that it stands tall in the picture.

Eight tiers §

The crown is eight tiers, stacked from the tip down. Each tier is a record of three fractions:

TreeShapes.roc
## One tier of the crown: where its top and its bottom are, as fractions of
## the crown's height down from its top, and how wide its bottom is, as a
## fraction of the crown's width.
Tier : {
	top : Fraction,
	bottom : Fraction,
	width : Fraction,
}

top and bottom say where the tier starts and ends, as shares of the crown's height, measured down from the tip. width says how wide the tier's bottom is, as a share of the crown at its widest.

The widths start as a list of eight plain numbers:

TreeShapes.roc
## How wide each tier's bottom is, top tier first, as a fraction of the crown's
## width, chosen by eye.
tier_widths : List(Fraction)
tier_widths = [0.35, 0.44, 0.53, 0.63, 0.72, 0.81, 0.91, 1.0].map(Fraction.of)

In a type, square brackets list tags. In a value, as here, they make a list, and every item in a list has the same type. These eight are F64s, chosen by eye.

map calls a function on each item of a list and returns a list of the answers. Here we hand it Fraction.of by name, and Fraction.of turns an F64 into a Fraction, so tier_widths is a List(Fraction).

Tops and bottoms §

Each tier also needs a top and a bottom. They follow a rule, so another map works them out:

TreeShapes.roc
## The tiers, top to bottom. Each starts a tenth of the crown lower than the
## one above and runs three tenths down, so each overlaps the next.
tiers : List(Tier)
tiers = tier_widths.map_with_index(
	|width, index| {
		top: Fraction.of(index.to_f64() / 10.0),
		bottom: Fraction.of((index.to_f64() + 3.0) / 10.0),
		width,
	},
)

map_with_index is map with one more thing for the function: the item's place in the list, counting from 0. The function here is written in place, and returns a record. Each tier's top is a tenth of the crown lower than the top of the tier above, and its bottom is three tenths below its top, so each tier overlaps the next.

Freezing the tiers §

tiers is a constant, the same eight records every time the program runs. So we can write all eight down, and have Roc check them:

TreeShapes.roc, the expect after tiers
# Each tier's top, bottom and width, top tier first.
expect
	tiers.map(|tier| [tier.top.to_f64(), tier.bottom.to_f64(), tier.width.to_f64()])
		== [
			[0.0, 0.3, 0.35],
			[0.1, 0.4, 0.44],
			[0.2, 0.5, 0.53],
			[0.3, 0.6, 0.63],
			[0.4, 0.7, 0.72],
			[0.5, 0.8, 0.81],
			[0.6, 0.9, 0.91],
			[0.7, 1.0, 1.0],
		]

An expect is a test. Its body is a Bool, and roc test runs every expect in a program and reports each one that comes out False. Its table is the tiers, written out by hand. Change a width or the rule, and the test reports it.

A triangle for each tier §

A tier is a set of shares. A triangle is corners on the screen. Between the two stands the tree's outline: where its foot lands on the screen, and how tall the tree looks there.

Camera.roc
## An upright object on the screen: where its foot is, and how far up the
## screen its top is from there. A tree, a still and a billboard all stand
## this way.
Upright : { foot : Point, height_on_screen : Pixels }

The camera works the outline out from where the tree stands, by perspective, which the “The math layer” chapter explains near the end of the book. Three small functions read the crown's place from the outline:

TreeShapes.roc
crown_top_y : Camera.Upright -> Pixels
crown_height : Camera.Upright -> Pixels
crown_half_width : Camera.Upright -> Pixels

crown_top_y gives the tree's tip, and the other two are fixed shares of the tree's height on the screen, chosen by eye. Then tier_triangle makes one tier:

TreeShapes.roc
## A tier as a flat triangle: its apex at the tier's top, its base across its
## bottom.
tier_triangle : Camera.Upright, { tier : Tier, foliage_color : RgbColor } -> Shapes.Shape
tier_triangle = |outline, { tier, foliage_color }| {
	crown_top = crown_top_y(outline)
	bottom_y = crown_top.plus(crown_height(outline).scaled_by(tier.bottom))
	top_y = crown_top.plus(crown_height(outline).scaled_by(tier.top))
	half_base = crown_half_width(outline).scaled_by(tier.width)
	Triangle({
		first: { x: outline.foot.x, y: top_y },
		second: { x: outline.foot.x.plus(half_base), y: bottom_y },
		third: { x: outline.foot.x.minus(half_base), y: bottom_y },
		fill: Brush.flat(foliage_color),
	})
}

It takes the outline and a record, and |outline, { tier, foliage_color }| takes the record apart into its fields, as Shapes.rectangle did. Like Distance, Pixels has its own plus, minus and scaled_by, which multiplies a length by a Fraction.

On the screen, y grows downward, as it did in the “Shapes” chapter, so plus moves down from the tip.

Painting the tree §

The trunk is a rectangle:

TreeShapes.roc
trunk_shape : Camera.Upright, { finish : TrunkFinish, color : RgbColor } -> Shapes.Shape

It hands Shapes.rectangle a width, a height and a top left corner from the outline. Its finish says whether the trunk is shaded.

tree_sprite produces the whole tree's sprite, and the order of its shapes matters:

TreeShapes.roc
## The tree on the screen: its trunk, then its tiers top to bottom. A tree
## within round_trunk_reach has a shaded trunk. The crown is cones, shaded by
## how near they are, within cone_reach, and triangles beyond it.
tree_sprite : Sprite.Setting, { height : Distance, foliage_color : RgbColor } -> Sprite
tree_sprite = |{ spot, focal_length, darkness }, { height, foliage_color }| {
	dimmed_foliage = Sky.color_at_darkness(foliage_color, darkness)
	inputs = tier_inputs_of(spot, { height, focal_length, foliage_color: dimmed_foliage })
	forward = inputs.base.forward()
	crown = TreeShapes.crown_of(forward)
	trunk = trunk_shape(
		inputs.outline,
		{
			finish: trunk_finish_of(forward),
			color: Sky.color_at_darkness(trunk_color, darkness),
		},
	)
	Sprite.from_shapes([trunk].concat(tiers.map(|tier| tier_shape(tier, { inputs, crown }))))
}

The body first darkens the foliage and the trunk's brown as dusk falls. Then tier_inputs_of works out what every tier needs, once for the whole tree. From how far ahead the tree's foot stands, crown_of and trunk_finish_of pick how much detail it gets: for a far tree, crown is Triangles, and the trunk's finish is Flat. The end of the chapter says what nearer trees get.

The last line puts the list together. [trunk] is a list of one shape, and concat joins two lists into one, so the trunk comes first. The platform paints a list from first to last, so the tiers land on the trunk. Each tier also lands on the tier above it and covers that tier's bottom, which is where the overlap goes.

tiers.map(...) makes one shape for each tier with tier_shape, whose match on the crown has a Triangles branch for the far tree:

TreeShapes.roc, in tier_shape
# ...
Triangles => tier_triangle(inputs.outline, colored_tier)
# ...

colored_tier is the tier with the tree's foliage color, so a far tree's tier goes straight to tier_triangle. The other branch builds the near tree's tiers.

What you're missing §

tree_sprite builds this way every tree at least cone_reach, 200 metres, ahead of the camera. Nearer trees get more. Each tier becomes a cone, which looks round and fattens as the rider passes under it, and the nearest cones are shaded. crown_of makes that choice, and tier_shape builds the cones. A cone on the screen is the outline round its ring and its tip, which the ConvexHull module finds.

Within round_trunk_reach, 60 metres, the trunk's finish is Shaded, and the trunk looks round.

Cones take far more work to build, and far off they look just like triangles, so the ride spends that work only where it shows.

So a far tree is a handful of small functions over one list of tiers. Every tree beside the road runs through this same code, wherever it stands. The next chapter follows the trip around that code, from a place on the road to the frame.