modules/colour/contrast.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/contrast - Colour contrast helpers.

=head1 SYNOPSIS

  from colour/contrast import
      contrast_ratio,
      contrast_passes,
      readable_text_colour;

  say( contrast_ratio( "black", "white" ) );
  say( readable_text_colour( "gold" ) );

=head1 DESCRIPTION

This pure-Zuzu module provides WCAG-style relative luminance and contrast
ratio helpers. All colour arguments are parsed through C<std/colour> via
C<colour/palette>.

=head1 EXPORTED FUNCTIONS

=over

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

Return the sRGB relative luminance for C<colour>.

=item * C<< contrast_ratio(String foreground, String background) >>

Return the contrast ratio between two colours.

=item * C<< contrast_passes(String foreground, String background,
String level := "AA", Boolean large_text := false) >>

Return whether the pair passes C<AA> or C<AAA>. Large text uses the
corresponding reduced thresholds.

=item * C<< contrast_grade(String foreground, String background) >>

Return C<AAA>, C<AA>, C<AA Large>, or C<Fail>.

=item * C<< best_contrast(String background, Array colours) >>

Return the candidate colour with the best contrast against C<background>.

=item * C<< readable_text_colour(String background, String dark :=
"#000000", String light := "#ffffff") >>

Return whichever of C<dark> or C<light> has better contrast against
C<background>.

=back

=head1 COPYRIGHT AND LICENCE

B<< colour/contrast >> 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 colour/palette import rgb;

function _linear_component ( Number channel ) {
	let value := channel / 255;
	if ( value <= 0.03928 ) {
		return value / 12.92;
	}
	return Math.pow( ( value + 0.055 ) / 1.055, 2.4 );
}

function relative_luminance ( String colour ) {
	let parts := rgb(colour);
	return 0.2126 * _linear_component( parts{r} )
		+ 0.7152 * _linear_component( parts{g} )
		+ 0.0722 * _linear_component( parts{b} );
}

function contrast_ratio ( String foreground, String background ) {
	let left := relative_luminance(foreground);
	let right := relative_luminance(background);
	let lighter := Math.max( left, right );
	let darker := Math.min( left, right );
	return ( lighter + 0.05 ) / ( darker + 0.05 );
}

function _threshold ( String level, Boolean large_text ) {
	let normalized := uc(level);
	if ( normalized eq "AA" ) {
		return large_text ? 3 : 4.5;
	}
	if ( normalized eq "AAA" ) {
		return large_text ? 4.5 : 7;
	}
	die "colour/contrast: level must be AA or AAA";
}

function contrast_passes (
	String foreground,
	String background,
	String level := "AA",
	Boolean large_text := false
) {
	return contrast_ratio( foreground, background )
		>= _threshold( level, large_text );
}

function contrast_grade ( String foreground, String background ) {
	let ratio := contrast_ratio( foreground, background );
	if ( ratio >= 7 ) {
		return "AAA";
	}
	if ( ratio >= 4.5 ) {
		return "AA";
	}
	if ( ratio >= 3 ) {
		return "AA Large";
	}
	return "Fail";
}

function best_contrast ( String background, Array colours ) {
	if ( colours.length() == 0 ) {
		die "colour/contrast: colours must not be empty";
	}

	let best := parse_colour( "" _ colours[0] );
	let best_ratio := contrast_ratio( best, background );
	let i := 1;

	while ( i < colours.length() ) {
		let candidate := parse_colour( "" _ colours[i] );
		let ratio := contrast_ratio( candidate, background );
		if ( ratio > best_ratio ) {
			best := candidate;
			best_ratio := ratio;
		}
		i++;
	}

	return best;
}

function readable_text_colour (
	String background,
	String dark := "#000000",
	String light := "#ffffff"
) {
	return best_contrast( background, [ dark, light ] );
}