Colour Contrast
02 Feb 2007
As some may know, I'm in the process of redesigning this site, triggered in part because of the totally inaccessible manner in which it is displayed in IE7 (I would usually ignore that but it's one thing to not look too good, it's quite another to put the buttons in places that are unreachable). So, IE7 isn't all bad, it gave me something to investigate :). As part of the redesign I've been looking at ways of 'designing' my CSS files such that they can be easily edited and, as an added benefit, provides a means for user customization; in doing so I came across a problem.
Explaining the problem
The monitor I have at work is crap. It's old, it's small and it hurts my eyes unless I'm reading 20pt+ text from 2 meters away with frequent coffee breaks. Another of the reasons that I am redesigning my site is to ensure I can still read/work with it whilst using a low colour remote connection - I make almost constant use of Ctrl(+) whilst remote connected to aid readability; and on my own site the none-graphic alternate CSS. I'm not alone in my quest for unstrained eyes of course, there are vast numbers of people who, for varying reasons, need or want either larger screen text or higher contrast web content. As an example, I think it was Jippi that mentioned to me about Php throwdown, a wonderful idea (and I hope that the cake team do well with the Itemizr application that they managed to build in 24 hours - how many coffees is that!), and a site with a great fresh design. At the time i was on a remote connection and just clicked the link - it literally took me a couple of minutes to see that the comment section had a comment form. I wasn't actively looking for it, but whilst reading some comments it dawned on me that "name" was in a box with some text above it. I had to highlight the text to be able to read it - the difference between #555555 and #333333 (can you read that?) was barely visible.
The solution
There's no substitute for common sense and manually looking at the difference between two colours and checking if you can read it. In addition there areW3 accessibility recommendations regarding the difference in brightness and contrast between two colours. You can see how the W3 calculation works and test for yourself the numerical differences on the Snook Colour Contrast Check. No matter, putting common sense on the shelf, it is possible to entrust a digital mind to calculate (as a guideline) if the contrast and difference in brightness between two colours is sufficient to be able to read text/distinguish content. I searched around for a php class to do the simple calculations for me, but didn't find anything, I did find a few js implementations. Irgo: the colour manipulation class was born. There is unlikely to be anything new or incredibly groundbreaking in what I am presenting here, and I present it here for primarily for comment.
Below is my standalone (hex) colour manipulation class:
{
<?php
/* SVN FILE: $Id: colour_manipulator.php 28 2007-02-08 20:51:38Z Andy $ */
/**
* Functions for manipulating RGB colours
*
* Generic functions realted to colour manipulation
*
* Copyright (c), Andy Dawson
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @filesource
* @copyright Copyright (c) 2007, Andy Dawson
* @package noswad
* @subpackage noswad.vendors
* @since Noswad site version 3
* @version $Revision: 28 $
* @created 26/01/2007
* @modifiedby $LastChangedBy$
* @lastmodified $Date: 2007-02-08 21:51:38 +0100 (jue, 08 feb 2007) $
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
/**
* Colour manipulator class
*
* Class for manipulation of (hex) colour codes primarily for use in serving dynamic and customizable CSS files
*
* @package noswad
* @subpackage noswad.vendors
*/
class ColourManipulator {
/**
* return a list of integer values for (red, green, blue) for a hex colour code
*
* @param string $colour hex triplet
* @return array (red, green,blue) integer values
*/
function explode($colour) {
if (strlen($colour) == 4) {
$r= hexdec(substr($colour, 1, 1));
$g= hexdec(substr($colour, 2, 1));
$b= hexdec(substr($colour, 3, 1));
}
elseif (strlen($colour) == 7) {
$r= hexdec(substr($colour, 1, 2));
$g= hexdec(substr($colour, 3, 2));
$b= hexdec(substr($colour, 5, 2));
} else { // Bullet proofing
return array (
0,
0,
0
);
}
return array (
$r,
$g,
$b
);
}
/**
* return a colour hex triplet for a list of (red, green, blue) values
*
* @param integer $r red integer value (0-255)
* @param integer $g green integer value (0-255)
* @param integer $b blue integer value (0-255)
* @return string hex triplet
*/
function implode($r, $g, $b) {
return '#' .
str_pad(dechex($r), 2, '0', STR_PAD_LEFT) .
str_pad(dechex($g), 2, '0', STR_PAD_LEFT) .
str_pad(dechex($b), 2, '0', STR_PAD_LEFT);
}
/**
* return the opposite colour hex triplet to the passed colour hex triplet. E.g. returns black if white is passed.
* Works by generating a full colour pallette based on the passed colour, and returning the colour numerically
* opposite in the spectrum. e.g. if dark blue is passed, the return will be light blue; If very dark blue is
* passed it will return very pale blue. Will not necessarily result in a colour that will have significant
* contrast, or be aestetically pleasing, to the original colour.
*
* @param string $colour string hex triplet
* @return string hex triplet
*/
function opposite($colour) {
$pallette= ColourManipulator :: pallette($colour, array (
'direction' => 'both'
));
if (($colour!='#000000')&&(low($colour)!='#ffffff')) {
foreach ($pallette as $key => $pColour) {
if (low($colour) == low($pColour)) {
$wantedKey= count($pallette) - $key -1;
return $pallette[$wantedKey];
}
}
}
// No match was found with the above logic, or it's white or black
list ($r, $g, $b)= ColourManipulator :: explode($colour);
return ColourManipulator :: implode((abs(255 - $r)), (abs(255 - $g)), (abs(255 - $b)));
}
/**
* return the most vibrant shade of the passed colour. Works by increasing proportionally the r,g,b values
* until one of them is the maximum. e.g. passing '#7F6611' will return '#FFCC22'
*
* @param string $colour string hex triplet
* @return string hex triplet
*/
function maximize($colour) {
list ($r, $g, $b)= ColourManipulator :: explode($colour);
$max = max($r, $g, $b);
if ($max) {
$factor = 255/$max;
return ColourManipulator :: implode((abs($r*$factor)), (abs($g*$factor)), (abs($b*$factor)));
} else {
return '#ffffff';
}
}
/**
* Determine the brightness of a colour according to http://www.w3.org/TR/AERT#color-contrast
* a return value of 0 corresponds to black
* a return value of 255 corresponds to white
* with all variations inbetween.
*
* @param string $colour hex triplet
* @return integer 0 - 255
*/
function brightness($colour) {
list ($r, $g, $b)= ColourManipulator :: explode($colour);
return ($r * 299 + $g * 587 + $b * 114) / 1000;
}
/**
* Calculate the contrast between two colours according to http://www.w3.org/TR/AERT#color-contrast
*
* @param string $colour1 hex triplet
* @param string $colour2 hex triplet
* @return integer 0 - 765
*/
function contrast($colour1, $colour2) {
list ($r1, $g1, $b1)= ColourManipulator :: explode($colour1);
list ($r2, $g2, $b2)= ColourManipulator :: explode($colour2);
return (abs($r1 - $r2)) + (abs($g1 - $g2)) + (abs($b1 - $b2));
}
/**
* Based upon a single colour, return a colour pallete
*
* The params array can have the following values and meanings:
* shades: integer 0- 362 Determines the number of shades to return. The number of results may not match the
* requested number of shades if duplicate colours are produced during the calculation, or a
* calculated shade does not meet the minimum contrast requirement.
* direction: string 'darken', 'lighten', 'both' or null. If the value is null the direction will be determined
* to give the greatest contrast. e.g. passing #111111, will result in an array of colours which will
* end with white (#FFFFFF). If the value 'both' is passed, the number of shades returned will be set
* to the maximum, and a spectrum from black to white will be returned.
* minContrast: Integer 0 - 255. If specified, the difference between calculated shades is checked and only if
* the colour has a contrast greater the previuos shade in the sequence will it be added to the result
* set.
* maximize: boolean. maximize the passed shade before generating the pallette
*
* @param string $colour hex triplet
* @param array $params see above for description
* @return array an array of colour hex triplets
*/
function pallette($colour, $params=array()) {
$shades= isset ($params['shades']) ? $params['shades'] : 10;
$direction= isset ($params['direction']) ? $params['direction'] : null;
$minContrast= isset ($params['minContrast']) ? $params['minContrast'] : null;
$maximize= isset ($params['maximize']) ? $params['maximize'] : true;
if ($colour!='#000000'&&$maximize) {
$colour= ColourManipulator :: maximize($colour);
}
if ($direction == 'both') {
$params['shades']= 362; // Guess at maximum shades
$params['direction']= 'lighten';
$results= ColourManipulator :: pallette($colour, $params);
$params['direction']= 'darken';
$results= am($results, ColourManipulator :: pallette($colour, $params));
$results= array_unique($results);
sort($results);
return $results;
}
list ($r, $g, $b)= ColourManipulator :: explode($colour);
if (!$direction) {
if (ColourManipulator :: brightness($colour) > 125) {
// It's a bright colour, so darken
$direction= 'darken';
} else {
// It's a dark colour, so lighten
$direction= 'lighten';
}
}
if ($direction == 'darken') {
$colour2 = '#000000';
} else {
$colour2 = '#ffffff';
}
return ColourManipulator :: blend ($colour,$colour2,$params);
}
/**
* Based upon a two colours, return a colour pallete from one colour to the other
*
* The params array can have the following values and meanings:
* shades: integer 0- 362 Determines the number of shades to return. The number of results may not match the
* requested number of shades if duplicate colours are produced during the calculation, or a
* calculated shade does not meet the minimum contrast requirement.
* minContrast: Integer 0 - 255. If specified, the difference between calculated shades is checked and only if
* the colour has a contrast greater the previous shade in the sequence will it be added to the result
* set.
*
* @param string $colour hex triplet
* @param array $params see above for description
* @return array an array of colour hex triplets
*/
function blend($colour1,$colour2, $params=array()) {
$shades= isset ($params['shades']) ? $params['shades'] : 10;
$minContrast= isset ($params['minContrast']) ? $params['minContrast'] : null;
list ($r1, $g1, $b1)= ColourManipulator :: explode($colour1);
list ($r2, $g2, $b2)= ColourManipulator :: explode($colour2);
$r_part= - ($r1-$r2) / $shades;
$g_part= - ($g1-$g2) / $shades;
$b_part= - ($b1-$b2) / $shades;
if ($minContrast) {
$comparison= $colour1;
}
$results[]= $colour1;
for ($i= 1; $i <= $shades; $i++) {
$shade= ColourManipulator :: implode(($r1 + $i * $r_part), ($g1 + $i * $g_part), ($b1 + $i * $b_part));
if ($minContrast) {
if (ColourManipulator :: contrast($comparison, $shade) >= $minContrast) {
$comparison= $shade;
if (!in_array($shade, $results)) {
$results[]= $shade;
}
}
} else {
if (!in_array($shade, $results)) {
$results[]= $shade;
}
}
}
if (!in_array(low($colour2), $results)) {
$results[]= $colour2;
}
return $results;
}
}
?>
<?php/* SVN FILE: $Id: colour_manipulator.php 28 2007-02-08 20:51:38Z Andy $ *//*** Functions for manipulating RGB colours** Generic functions realted to colour manipulation** Copyright (c), Andy Dawson** Licensed under The MIT License* Redistributions of files must retain the above copyright notice.** @filesource* @copyright Copyright (c) 2007, Andy Dawson* @package noswad* @subpackage noswad.vendors* @since Noswad site version 3* @version $Revision: 28 $* @created 26/01/2007* @modifiedby $LastChangedBy$* @lastmodified $Date: 2007-02-08 21:51:38 +0100 (jue, 08 feb 2007) $* @license http://www.opensource.org/licenses/mit-license.php The MIT License*//*** Colour manipulator class** Class for manipulation of (hex) colour codes primarily for use in serving dynamic and customizable CSS files** @package noswad* @subpackage noswad.vendors*/class ColourManipulator {/*** return a list of integer values for (red, green, blue) for a hex colour code** @param string $colour hex triplet* @return array (red, green,blue) integer values*/function explode($colour) {if (strlen($colour) == 4) {$r= hexdec(substr($colour, 1, 1));$g= hexdec(substr($colour, 2, 1));$b= hexdec(substr($colour, 3, 1));}elseif (strlen($colour) == 7) {$r= hexdec(substr($colour, 1, 2));$g= hexdec(substr($colour, 3, 2));$b= hexdec(substr($colour, 5, 2));} else { // Bullet proofingreturn array (0,0,0);}return array ($r,$g,$b);}/*** return a colour hex triplet for a list of (red, green, blue) values** @param integer $r red integer value (0-255)* @param integer $g green integer value (0-255)* @param integer $b blue integer value (0-255)* @return string hex triplet*/function implode($r, $g, $b) {return '#' .str_pad(dechex($r), 2, '0', STR_PAD_LEFT) .str_pad(dechex($g), 2, '0', STR_PAD_LEFT) .str_pad(dechex($b), 2, '0', STR_PAD_LEFT);}/*** return the opposite colour hex triplet to the passed colour hex triplet. E.g. returns black if white is passed.* Works by generating a full colour pallette based on the passed colour, and returning the colour numerically* opposite in the spectrum. e.g. if dark blue is passed, the return will be light blue; If very dark blue is* passed it will return very pale blue. Will not necessarily result in a colour that will have significant* contrast, or be aestetically pleasing, to the original colour.** @param string $colour string hex triplet* @return string hex triplet*/function opposite($colour) {$pallette= ColourManipulator :: pallette($colour, array ('direction' => 'both'));if (($colour!='#000000')&&(low($colour)!='#ffffff')) {foreach ($pallette as $key => $pColour) {if (low($colour) == low($pColour)) {$wantedKey= count($pallette) - $key -1;return $pallette[$wantedKey];}}}// No match was found with the above logic, or it's white or blacklist ($r, $g, $b)= ColourManipulator :: explode($colour);return ColourManipulator :: implode((abs(255 - $r)), (abs(255 - $g)), (abs(255 - $b)));}/*** return the most vibrant shade of the passed colour. Works by increasing proportionally the r,g,b values* until one of them is the maximum. e.g. passing '#7F6611' will return '#FFCC22'** @param string $colour string hex triplet* @return string hex triplet*/function maximize($colour) {list ($r, $g, $b)= ColourManipulator :: explode($colour);$max = max($r, $g, $b);if ($max) {$factor = 255/$max;return ColourManipulator :: implode((abs($r*$factor)), (abs($g*$factor)), (abs($b*$factor)));} else {return '#ffffff';}}/*** Determine the brightness of a colour according to http://www.w3.org/TR/AERT#color-contrast* a return value of 0 corresponds to black* a return value of 255 corresponds to white* with all variations inbetween.** @param string $colour hex triplet* @return integer 0 - 255*/function brightness($colour) {list ($r, $g, $b)= ColourManipulator :: explode($colour);return ($r * 299 + $g * 587 + $b * 114) / 1000;}/*** Calculate the contrast between two colours according to http://www.w3.org/TR/AERT#color-contrast** @param string $colour1 hex triplet* @param string $colour2 hex triplet* @return integer 0 - 765*/function contrast($colour1, $colour2) {list ($r1, $g1, $b1)= ColourManipulator :: explode($colour1);list ($r2, $g2, $b2)= ColourManipulator :: explode($colour2);return (abs($r1 - $r2)) + (abs($g1 - $g2)) + (abs($b1 - $b2));}/*** Based upon a single colour, return a colour pallete** The params array can have the following values and meanings:* shades: integer 0- 362 Determines the number of shades to return. The number of results may not match the* requested number of shades if duplicate colours are produced during the calculation, or a* calculated shade does not meet the minimum contrast requirement.* direction: string 'darken', 'lighten', 'both' or null. If the value is null the direction will be determined* to give the greatest contrast. e.g. passing #111111, will result in an array of colours which will* end with white (#FFFFFF). If the value 'both' is passed, the number of shades returned will be set* to the maximum, and a spectrum from black to white will be returned.* minContrast: Integer 0 - 255. If specified, the difference between calculated shades is checked and only if* the colour has a contrast greater the previuos shade in the sequence will it be added to the result* set.* maximize: boolean. maximize the passed shade before generating the pallette** @param string $colour hex triplet* @param array $params see above for description* @return array an array of colour hex triplets*/function pallette($colour, $params=array()) {$shades= isset ($params['shades']) ? $params['shades'] : 10;$direction= isset ($params['direction']) ? $params['direction'] : null;$minContrast= isset ($params['minContrast']) ? $params['minContrast'] : null;$maximize= isset ($params['maximize']) ? $params['maximize'] : true;if ($colour!='#000000'&&$maximize) {$colour= ColourManipulator :: maximize($colour);}if ($direction == 'both') {$params['shades']= 362; // Guess at maximum shades$params['direction']= 'lighten';$results= ColourManipulator :: pallette($colour, $params);$params['direction']= 'darken';$results= am($results, ColourManipulator :: pallette($colour, $params));$results= array_unique($results);sort($results);return $results;}list ($r, $g, $b)= ColourManipulator :: explode($colour);if (!$direction) {if (ColourManipulator :: brightness($colour) > 125) {// It's a bright colour, so darken$direction= 'darken';} else {// It's a dark colour, so lighten$direction= 'lighten';}}if ($direction == 'darken') {$colour2 = '#000000';} else {$colour2 = '#ffffff';}return ColourManipulator :: blend ($colour,$colour2,$params);}/*** Based upon a two colours, return a colour pallete from one colour to the other** The params array can have the following values and meanings:* shades: integer 0- 362 Determines the number of shades to return. The number of results may not match the* requested number of shades if duplicate colours are produced during the calculation, or a* calculated shade does not meet the minimum contrast requirement.* minContrast: Integer 0 - 255. If specified, the difference between calculated shades is checked and only if* the colour has a contrast greater the previous shade in the sequence will it be added to the result* set.** @param string $colour hex triplet* @param array $params see above for description* @return array an array of colour hex triplets*/function blend($colour1,$colour2, $params=array()) {$shades= isset ($params['shades']) ? $params['shades'] : 10;$minContrast= isset ($params['minContrast']) ? $params['minContrast'] : null;list ($r1, $g1, $b1)= ColourManipulator :: explode($colour1);list ($r2, $g2, $b2)= ColourManipulator :: explode($colour2);$r_part= - ($r1-$r2) / $shades;$g_part= - ($g1-$g2) / $shades;$b_part= - ($b1-$b2) / $shades;if ($minContrast) {$comparison= $colour1;}$results[]= $colour1;for ($i= 1; $i <= $shades; $i++) {$shade= ColourManipulator :: implode(($r1 + $i * $r_part), ($g1 + $i * $g_part), ($b1 + $i * $b_part));if ($minContrast) {if (ColourManipulator :: contrast($comparison, $shade) >= $minContrast) {$comparison= $shade;if (!in_array($shade, $results)) {$results[]= $shade;}}} else {if (!in_array($shade, $results)) {$results[]= $shade;}}}if (!in_array(low($colour2), $results)) {$results[]= $colour2;}return $results;}}?>
As I labored intensely :) adding documentation, I'll only say that it permits the possibility to determine brightness, contrast and derive a palette of shades of a colour (even a full palette from black to white through the original colour) from a passed hex colour code.
The use
Many sites, google groups for example, provide the possibility to use a standard template but change the colour scheme used. Other sites, dictionary.com for example, use the same principle to subtle distinguish between site sections whilst maintaining continuity. If a colour is derived or user customizable, it is quite possible to specify a colour scheme which is either difficult or impossible to read. By making use of the calculation defined in the w3 accessibility guidelines, it is possible to determine if the contrast between two colours is sufficient to allow reading and either warn the user or take steps to combat the problem (such as switching from a white to a black background). I'm still experimenting with my (php-ed) css files, but by specifying a single 'theme' colour it is possible to generate a palette of shades for use and, in addition, ensure that the text and backgrounds have sufficient contrast to be readable.
Wrapping Up
Provided here is a simple class which calculates the brightness of a colour; the contrast between two colours; the 'opposite' of a colour; or generates a colour palette (even with contrast restrictions). So, now all I need to do is decide on that one colour I want my site to be based upon... that green on Cheesecake's site looks a bit too similar, I wonder what the colour contrast value is :D.
stabb, on 11/2/07
ges, on 26/8/07
Andy, on 2/9/07
Hi Ges,
It seems I made a silly mistake, the class is generic but I only ever call it myself from within a cake application so I hadn't noticed. The low function is part of cake's core, but as it's just an alias for strtolower(), you can simply replace it with the direct call.
hth