modules/colour/palette.zzm

colour-palette-0.0.1 source code

Package

Name
colour-palette
Version
0.0.1
Uploaded
2026-05-13 17:33:17
Dependencies
Metadata
zuzu-distribution.json
Archive
Download .tar.gz
=encoding utf8

=head1 NAME

colour/palette - Colour palette helpers.

=head1 SYNOPSIS

  from colour/palette import
      rgb,
      hsl,
      from_rgb,
      from_hsl,
      lighten,
      complement,
      triad;

  say( rgb("purple") );
  say( lighten( "#336699", 10 ) );
  say( triad( "tomato" ) );

=head1 DESCRIPTION

This pure-Zuzu module builds on C<std/colour> to convert colours between
hexadecimal, RGB, and HSL forms, adjust lightness and saturation, mix two
colours, rotate hue, and generate small palettes.

All colour arguments are parsed with C<parse_colour>, so CSS colour
keywords and three- or six-digit hexadecimal colours are accepted.

=head1 EXPORTED FUNCTIONS

=over

=item * C<< rgb(String colour) >>

Return a dictionary with C<r>, C<g>, and C<b> channel values from 0 to
255.

=item * C<< hsl(String colour) >>

Return a dictionary with C<h> in degrees and C<s> and C<l> as
percentages.

=item * C<< from_rgb(Number r, Number g, Number b) >>

Return a normalized lowercase hexadecimal colour.

=item * C<< from_hsl(Number h, Number s, Number l) >>

Return a normalized lowercase hexadecimal colour.

=item * C<< mix(String left, String right, Number weight := 0.5) >>

Mix two colours. C<weight> is the fraction of C<right> in the result.

=item * C<< lighten(String colour, Number amount) >>

=item * C<< darken(String colour, Number amount) >>

=item * C<< saturate(String colour, Number amount) >>

=item * C<< desaturate(String colour, Number amount) >>

Adjust HSL percentage channels by C<amount> percentage points.

=item * C<< rotate_hue(String colour, Number degrees) >>

=item * C<< complement(String colour) >>

=item * C<< analogous(String colour, Number angle := 30) >>

=item * C<< triad(String colour) >>

=item * C<< tetrad(String colour) >>

=item * C<< monochrome(String colour, Number steps := 5,
Number spread := 30) >>

=back

=head1 COPYRIGHT AND LICENCE

B<< colour/palette >> is copyright Toby Inkster.

It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.

=cut

from std/colour import parse_colour;
from std/math import Math;
from std/string import substr;

function _div_floor ( Number n, Number d ) {
	return floor( n / d );
}

function _mod ( Number n, Number d ) {
	return n - _div_floor( n, d ) * d;
}

function _hex_channel ( String hex, Number offset ) {
	return Math.hex2dec( substr( hex, offset, 2 ) ) + 0;
}

function _clamp ( Number value, Number low, Number high ) {
	if ( value < low ) {
		return low;
	}
	if ( value > high ) {
		return high;
	}
	return value;
}

function _channel ( Number value ) {
	return int( _clamp( round(value), 0, 255 ) );
}

function _percent ( Number value ) {
	return _clamp( value, 0, 100 );
}

function _hex_byte ( Number value ) {
	let text := Math.dec2hex( "" _ _channel(value) );
	if ( length text == 1 ) {
		return "0" _ text;
	}
	return text;
}

function _rgb_to_hex ( Number r, Number g, Number b ) {
	return "#" _ _hex_byte(r) _ _hex_byte(g) _ _hex_byte(b);
}

function _wrap_hue ( Number hue ) {
	let wrapped := _mod( hue, 360 );
	if ( wrapped < 0 ) {
		wrapped += 360;
	}
	return wrapped;
}

function _hue_to_rgb ( Number c, Number x, Number hprime ) {
	if ( hprime < 1 ) {
		return [ c, x, 0 ];
	}
	if ( hprime < 2 ) {
		return [ x, c, 0 ];
	}
	if ( hprime < 3 ) {
		return [ 0, c, x ];
	}
	if ( hprime < 4 ) {
		return [ 0, x, c ];
	}
	if ( hprime < 5 ) {
		return [ x, 0, c ];
	}
	return [ c, 0, x ];
}

function rgb ( String colour ) {
	let hex := parse_colour(colour);
	return {
		r: _hex_channel( hex, 1 ),
		g: _hex_channel( hex, 3 ),
		b: _hex_channel( hex, 5 ),
	};
}

function hsl ( String colour ) {
	let parts := rgb(colour);
	let r := parts{r} / 255;
	let g := parts{g} / 255;
	let b := parts{b} / 255;
	let max := Math.max( r, g, b );
	let min := Math.min( r, g, b );
	let delta := max - min;
	let lightness := ( max + min ) / 2;
	let hue := 0;
	let saturation := 0;

	if ( delta != 0 ) {
		saturation := delta / ( 1 - abs( 2 * lightness - 1 ) );

		if ( max == r ) {
			hue := 60 * _mod( ( g - b ) / delta, 6 );
		}
		else if ( max == g ) {
			hue := 60 * ( ( b - r ) / delta + 2 );
		}
		else {
			hue := 60 * ( ( r - g ) / delta + 4 );
		}
	}

	return {
		h: _wrap_hue(hue),
		s: saturation * 100,
		l: lightness * 100,
	};
}

function from_rgb ( Number r, Number g, Number b ) {
	return _rgb_to_hex( r, g, b );
}

function from_hsl ( Number h, Number s, Number l ) {
	let hue := _wrap_hue(h);
	let saturation := _percent(s) / 100;
	let lightness := _percent(l) / 100;

	if ( saturation == 0 ) {
		let grey := lightness * 255;
		return _rgb_to_hex( grey, grey, grey );
	}

	let c := ( 1 - abs( 2 * lightness - 1 ) ) * saturation;
	let hprime := hue / 60;
	let x := c * ( 1 - abs( _mod( hprime, 2 ) - 1 ) );
	let base := _hue_to_rgb( c, x, hprime );
	let m := lightness - c / 2;

	return _rgb_to_hex(
		( base[0] + m ) * 255,
		( base[1] + m ) * 255,
		( base[2] + m ) * 255,
	);
}

function mix ( String left, String right, Number weight := 0.5 ) {
	let lw := 1 - _clamp( weight, 0, 1 );
	let rw := 1 - lw;
	let l := rgb(left);
	let r := rgb(right);

	return _rgb_to_hex(
		l{r} * lw + r{r} * rw,
		l{g} * lw + r{g} * rw,
		l{b} * lw + r{b} * rw,
	);
}

function lighten ( String colour, Number amount ) {
	let value := hsl(colour);
	return from_hsl( value{h}, value{s}, value{l} + amount );
}

function darken ( String colour, Number amount ) {
	return lighten( colour, -amount );
}

function saturate ( String colour, Number amount ) {
	let value := hsl(colour);
	return from_hsl( value{h}, value{s} + amount, value{l} );
}

function desaturate ( String colour, Number amount ) {
	return saturate( colour, -amount );
}

function rotate_hue ( String colour, Number degrees ) {
	let value := hsl(colour);
	return from_hsl( value{h} + degrees, value{s}, value{l} );
}

function complement ( String colour ) {
	return rotate_hue( colour, 180 );
}

function analogous ( String colour, Number angle := 30 ) {
	return [
		rotate_hue( colour, -angle ),
		parse_colour(colour),
		rotate_hue( colour, angle ),
	];
}

function triad ( String colour ) {
	return [
		parse_colour(colour),
		rotate_hue( colour, 120 ),
		rotate_hue( colour, 240 ),
	];
}

function tetrad ( String colour ) {
	return [
		parse_colour(colour),
		rotate_hue( colour, 90 ),
		complement(colour),
		rotate_hue( colour, 270 ),
	];
}

function monochrome ( String colour, Number steps := 5, Number spread := 30 ) {
	let count := int(steps);
	if ( count != steps or count < 1 ) {
		die "colour/palette: steps must be a positive integer";
	}

	let value := hsl(colour);
	let start := value{l} - spread / 2;
	let out := [];
	let i := 0;

	while ( i < count ) {
		let offset := count == 1 ? 0 : spread * i / ( count - 1 );
		out.push( from_hsl( value{h}, value{s}, start + offset ) );
		i++;
	}

	return out;
}