Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

Chapter 14

The sky and the grass

Behind everything beside the road stands the backdrop: the sky above the horizon and the grass below it. The horizon on the screen is the line halfway down it, where a level look from the rider lands. In a frame the sky is two colors, one at the top and one at the horizon, with a fade between them, and the grass is one rectangle. At step 1,529 the sky's two colors happen to match, for a reason the end of this chapter gives.

Step 1,529 of the ride: the trees, the grass, and the sky over them, one day blue.

One function in the Sky module returns both colors at the ride's clock, as one record:

Sky.roc
## The sky's two colors at `clock`.
colors_at : Clock -> Sky.Colors
colors_at = |clock| { top: sky_top_color(clock), horizon: sky_horizon_color(clock) }

Sky.Colors is a record of two RgbColors, top and horizon. Each color has a function of its own.

The top of the sky §

The top of the sky blends from day into night, by how dark the sky is:

Sky.roc
## The sky's color at the top of the frame.
sky_top_color : Clock -> RgbColor
sky_top_color = |clock| ColorShade.color_between({
	from: day_sky,
	to: night_sky,
	fraction: Sky.darkness_at(clock),
})

day_sky and night_sky are two constants in the module, a pale blue and a deep one. Each constant carries an annotation, as a function does, and here the type is an RgbColor. Sky.darkness_at, from the “Functions” chapter, says how dark the sky is, as a Fraction. ColorShade.color_between blends the two colors:

lib/ColorShade.roc
## The color `fraction` of the way from `from` to `to`, each channel
## rounded to a whole value as `channel_between` rounds it.
color_between : {
	from : RgbColor,
	to : RgbColor,
	fraction : Fraction,
} -> RgbColor
color_between = |{ from, to, fraction }| {
	blend = |channel_of|
		channel_between({
			from: channel_of(from),
			to: channel_of(to),
			fraction,
		})
	RgbColor.from_channels({
		red: blend(RgbColor.red),
		green: blend(RgbColor.green),
		blue: blend(RgbColor.blue),
	})
}

It takes one record, so every call names its three fields. A color is three channels, its red, green and blue, and each channel moves the same share of the way from from to to. blend is a function named inside the body, and it takes a function as its argument, such as RgbColor.red, which reads one channel of a color. The last line calls blend once for each channel. The share is a Fraction, which its type keeps between 0 and 1, so each channel lands between its values in from and to.

The horizon §

The horizon has the top's color, warmed toward red while the sun is near it:

Sky.roc
## The sky's color at the horizon: the sky warmed toward sunset red.
sky_horizon_color : Clock -> RgbColor
sky_horizon_color = |clock| ColorShade.color_between({
	from: sky_top_color(clock),
	to: sunset_red,
	fraction: Sky.horizon_warmth_at(clock).times(horizon_warmth_at_most),
})

Sky.horizon_warmth_at says how near the sun stands to the horizon, as a Fraction, and horizon_warmth_at_most holds the warmest horizon to 85% of the way to sunset_red, so a little of the sky's own color still shows.

The fade §

Sky.sky_shape returns the two colors as one shape:

Sky.roc
## The sky in `colors`: a gradient from the top colour down to the
## horizon's, over the rectangle above the horizon.
sky_shape : Sky.Colors -> Shapes.Shape
sky_shape = |colors|
	Screen.above_horizon_rect(
		Linear({
			stops: {
				start_color: Brush.opaque_rgba(colors.top),
				end_color: Brush.opaque_rgba(colors.horizon),
				start_offset: sky_top_stop.to_f64(),
				end_offset: 1.0,
			},
			start_point: { x: Pixels.zero, y: Pixels.zero },
			end_point: { x: Pixels.zero, y: Screen.horizon_y },
		}),
	)

Screen.above_horizon_rect is the rectangle from above the top of the screen down to the horizon, 200 pixels past each side. A fill is a tag union, Brush.Fill, and Brush.flat makes its plainest tag, one color. This fill is its Linear tag, whose color fades along a line, from start_point to end_point. Both points sit at x zero, so the line runs straight down, from the top of the screen to the horizon. Its stops say where along that line the fade begins and ends: the fade holds the top color for the first fifth of the way, sky_top_stop, then reaches the horizon's color at the end. The platform works out every color in between. Brush.opaque_rgba makes each color fully solid.

The grass §

The grass is one rectangle below the horizon, in one color, which darkens with the sky:

Grass.roc
## The grass's colour under a sky `darkness` dark.
grass_color_at : Fraction -> RgbColor
grass_color_at = |darkness| Sky.color_at_darkness(Grass.grass_color, darkness)

Sky.color_at_darkness takes a color by day and how dark the sky is, and returns the color dimmed to match. Everything the sky lights dims through it, from the grass to the truck. Here's the rectangle:

Grass.roc
## The grass under a sky `darkness` dark: a rectangle from `grass_top_y`
## down to `roll_margin` below the screen, and `roll_margin` past each side.
grass_shape : Fraction -> Shapes.Shape
grass_shape = |darkness|
	Screen.margin_rect(
		{ top: grass_top_y, bottom: Screen.height.plus(Screen.roll_margin) },
		Brush.flat(Grass.grass_color_at(darkness)),
	)

The grass's top edge sits one pixel above the horizon, so its top row covers the sky's last one, and the two always meet. When the bike leans, the camera rolls with it, and the whole frame turns about the middle of the screen. A rectangle the size of the screen would turn with it and leave its corners bare. So Screen.margin_rect makes a rectangle that reaches Screen.roll_margin, 200 pixels, past each side of the screen, as the sky's does, and the grass reaches 200 pixels below the screen too. That covers a lean of up to 30°, more than the ride ever leans.

Every function here works from the clock alone, or from colors worked out from it. So the backdrop at step 1,529 is the same on every run of the ride. At that step the sun is still high: the sky is near its day color and the horizon has yet to warm, so the two colors match and the picture shows one pale blue. In the second half of the book the clock runs, and the two colors part. One more part of the frame darkens with it: the mountains.