Core/Palette.cs

Static Palette class that defines game-wide Color constants for environment, playfield, snake, apple, lighting, and a Segment method that linearly interpolates body-to-tail colors by index and length.

namespace Coilgarden;

/// <summary>
/// Every colour in the game world, in one place.
/// <para>
/// <b>The direction: a cozy garden tray on a table.</b> A shallow terracotta planter holding
/// pale sand, lit warmly from one side, with a deep green snake and one vivid red apple. The
/// palette is warm earth tones plus exactly two saturated accents, which is what lets it be
/// colourful and restrained at the same time.
/// </para>
/// <para>
/// The floor is <em>light</em> and the snake is <em>dark</em>. That inversion is deliberate:
/// it is the cozier and less generic of the two options, it gives the largest possible value
/// gap between the snake and the surface it moves on, and it lets the apple's red read
/// against a neutral rather than competing with a dark background.
/// </para>
/// <para>
/// Two hard rules, both learned the hard way:
/// </para>
/// <list type="number">
/// <item>Every channel stays within 0..1. Above 1 the tint path wraps rather than clamping
/// and the colour comes out completely wrong - a green of 1.30 rendered as purple.</item>
/// <item>Contrast comes from <em>value</em>, not saturation. Saturated colours next to each
/// other read as noise; the same hues separated by lightness read as clean.</item>
/// </list>
/// </summary>
public static class Palette
{
	// ---------------------------------------------------------------- environment

	/// <summary>Behind and around the tray. Warm and deep, so the tray reads as lit.</summary>
	public static readonly Color Backdrop = new( 0.082f, 0.070f, 0.086f );

	/// <summary>The table the tray sits on - a shade up from the backdrop, so the tray has a surface.</summary>
	public static readonly Color Table = new( 0.184f, 0.133f, 0.106f );

	/// <summary>The planter's outer wall.</summary>
	public static readonly Color TrayRim = new( 0.784f, 0.404f, 0.227f );

	/// <summary>The rim's inner face, which is turned away from the key light.</summary>
	public static readonly Color TrayRimInner = new( 0.478f, 0.259f, 0.176f );

	/// <summary>A thin lighter line along the top of the rim, so the edge catches the light.</summary>
	public static readonly Color TrayRimTop = new( 0.906f, 0.588f, 0.361f );

	// ---------------------------------------------------------------- the playfield

	/// <summary>Pale sand. The two soil tones differ by about 4% so the grid is legible but silent.</summary>
	public static readonly Color SoilLight = new( 1.000f, 0.965f, 0.878f );

	public static readonly Color SoilDark = new( 0.941f, 0.898f, 0.804f );

	// ---------------------------------------------------------------- the snake

	/// <summary>The head. Warmer and lighter than the body, so it is the first thing found.</summary>
	public static readonly Color SnakeHead = new( 0.325f, 0.686f, 0.310f );

	public static readonly Color SnakeBody = new( 0.169f, 0.494f, 0.251f );

	/// <summary>The tail end. Darker, so the body reads as travelling head-first in a still frame.</summary>
	public static readonly Color SnakeTail = new( 0.098f, 0.325f, 0.176f );

	/// <summary>
	/// Eyes. Nearly black rather than white-with-a-pupil: at this size a dark dot reads
	/// cleanly and a two-tone eye turns to mush.
	/// </summary>
	public static readonly Color SnakeEye = new( 0.086f, 0.106f, 0.098f );

	// ---------------------------------------------------------------- the apple

	/// <summary>
	/// The one high-chroma red in the game, used nowhere else, so the eye always finds the
	/// apple first without any effect being needed.
	/// </summary>
	public static readonly Color AppleFlesh = new( 0.949f, 0.216f, 0.157f );

	public static readonly Color AppleStem = new( 0.298f, 0.196f, 0.129f );

	public static readonly Color AppleLeaf = new( 0.510f, 0.729f, 0.251f );

	/// <summary>
	/// Sparkles thrown when an apple is eaten: the apple's own red, a shade deeper, so the burst
	/// reads as <em>that apple</em> coming apart rather than as a generic effect.
	/// <para>
	/// They were originally a pale salmon - the apple lightened towards the sand - and at that
	/// value they were completely invisible however correctly they simulated. On a light floor a
	/// burst has to be <em>darker</em> than what it lands on, not brighter.
	/// </para>
	/// </summary>
	public static readonly Color Sparkle = new( 0.878f, 0.243f, 0.122f );

	/// <summary>
	/// The head at the instant of death. Near-white so the moment of impact is unmissable, and
	/// it is the only near-white in the game.
	/// </summary>
	public static readonly Color DeathFlash = new( 1.000f, 0.949f, 0.906f );

	/// <summary>
	/// What the snake settles to after dying: a dark, dry olive, like a plant that has gone
	/// over. It wilts rather than vanishing, so the shape of the run the player just made is
	/// still on the tray behind the game-over card.
	/// <para>
	/// Kept clearly darker than the sand. A first attempt landed close to the sand's own value
	/// and the dead snake half-dissolved into the floor - which loses the whole point of leaving
	/// it there.
	/// </para>
	/// </summary>
	public static readonly Color SnakeWilt = new( 0.216f, 0.192f, 0.129f );

	// ---------------------------------------------------------------- lighting

	/// <summary>Warm key light, like late afternoon through a window.</summary>
	public static readonly Color KeyLight = new( 1.000f, 0.929f, 0.808f );

	/// <summary>Cool fill, so shadows are blue-grey rather than muddy brown.</summary>
	public static readonly Color FillLight = new( 0.255f, 0.310f, 0.408f );

	/// <summary>Ambient wash. Keeps shadowed sides readable instead of black.</summary>
	public static readonly Color Ambient = new( 0.478f, 0.435f, 0.400f );

	/// <summary>Vignette tint - the backdrop hue, so the edges recede rather than darken.</summary>
	public static readonly Color Vignette = new( 0.055f, 0.043f, 0.067f );

	/// <summary>
	/// The colour a segment is drawn in, by distance from the head.
	/// <para>
	/// The head is its own colour; the rest fade body-to-tail. The gradient is over the body
	/// only, so a two-segment snake still has a distinct head rather than being half-faded.
	/// </para>
	/// </summary>
	public static Color Segment( int index, int length )
	{
		if ( index <= 0 ) return SnakeHead;
		if ( length <= 2 ) return SnakeBody;

		var t = (index - 1) / (float)(length - 2);

		return Color.Lerp( SnakeBody, SnakeTail, t );
	}
}