Hide
on 2/2/07

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:

  1. <?php
  2. /* SVN FILE: $Id: colour_manipulator.php 28 2007-02-08 20:51:38Z Andy $ */
  3. /**
  4. * Functions for manipulating RGB colours
  5. *
  6. * Generic functions realted to colour manipulation
  7. *
  8. * Copyright (c), Andy Dawson
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * @filesource
  14. * @copyright Copyright (c) 2007, Andy Dawson
  15. * @package noswad
  16. * @subpackage noswad.vendors
  17. * @since Noswad site version 3
  18. * @version $Revision: 28 $
  19. * @created 26/01/2007
  20. * @modifiedby $LastChangedBy$
  21. * @lastmodified $Date: 2007-02-08 21:51:38 +0100 (jue, 08 feb 2007) $
  22. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  23. */
  24. /**
  25. * Colour manipulator class
  26. *
  27. * Class for manipulation of (hex) colour codes primarily for use in serving dynamic and customizable CSS files
  28. *
  29. * @package noswad
  30. * @subpackage noswad.vendors
  31. */
  32. class ColourManipulator {
  33. /**
  34. * return a list of integer values for (red, green, blue) for a hex colour code
  35. *
  36. * @param string $colour hex triplet
  37. * @return array (red, green,blue) integer values
  38. */
  39. function explode($colour) {
  40. if (strlen($colour) == 4) {
  41. $r= hexdec(substr($colour, 1, 1));
  42. $g= hexdec(substr($colour, 2, 1));
  43. $b= hexdec(substr($colour, 3, 1));
  44. }
  45. elseif (strlen($colour) == 7) {
  46. $r= hexdec(substr($colour, 1, 2));
  47. $g= hexdec(substr($colour, 3, 2));
  48. $b= hexdec(substr($colour, 5, 2));
  49. } else { // Bullet proofing
  50. return array (
  51. 0,
  52. 0,
  53. 0
  54. );
  55. }
  56. return array (
  57. $r,
  58. $g,
  59. $b
  60. );
  61. }
  62. /**
  63. * return a colour hex triplet for a list of (red, green, blue) values
  64. *
  65. * @param integer $r red integer value (0-255)
  66. * @param integer $g green integer value (0-255)
  67. * @param integer $b blue integer value (0-255)
  68. * @return string hex triplet
  69. */
  70. function implode($r, $g, $b) {
  71. return '#' .
  72. str_pad(dechex($r), 2, '0', STR_PAD_LEFT) .
  73. str_pad(dechex($g), 2, '0', STR_PAD_LEFT) .
  74. str_pad(dechex($b), 2, '0', STR_PAD_LEFT);
  75. }
  76. /**
  77. * return the opposite colour hex triplet to the passed colour hex triplet. E.g. returns black if white is passed.
  78. * Works by generating a full colour pallette based on the passed colour, and returning the colour numerically
  79. * opposite in the spectrum. e.g. if dark blue is passed, the return will be light blue; If very dark blue is
  80. * passed it will return very pale blue. Will not necessarily result in a colour that will have significant
  81. * contrast, or be aestetically pleasing, to the original colour.
  82. *
  83. * @param string $colour string hex triplet
  84. * @return string hex triplet
  85. */
  86. function opposite($colour) {
  87. $pallette= ColourManipulator :: pallette($colour, array (
  88. 'direction' => 'both'
  89. ));
  90. if (($colour!='#000000')&&(low($colour)!='#ffffff')) {
  91. foreach ($pallette as $key => $pColour) {
  92. if (low($colour) == low($pColour)) {
  93. $wantedKey= count($pallette) - $key -1;
  94. return $pallette[$wantedKey];
  95. }
  96. }
  97. }
  98. // No match was found with the above logic, or it's white or black
  99. list ($r, $g, $b)= ColourManipulator :: explode($colour);
  100. return ColourManipulator :: implode((abs(255 - $r)), (abs(255 - $g)), (abs(255 - $b)));
  101. }
  102. /**
  103. * return the most vibrant shade of the passed colour. Works by increasing proportionally the r,g,b values
  104. * until one of them is the maximum. e.g. passing '#7F6611' will return '#FFCC22'
  105. *
  106. * @param string $colour string hex triplet
  107. * @return string hex triplet
  108. */
  109. function maximize($colour) {
  110. list ($r, $g, $b)= ColourManipulator :: explode($colour);
  111. $max = max($r, $g, $b);
  112. if ($max) {
  113. $factor = 255/$max;
  114. return ColourManipulator :: implode((abs($r*$factor)), (abs($g*$factor)), (abs($b*$factor)));
  115. } else {
  116. return '#ffffff';
  117. }
  118. }
  119. /**
  120. * Determine the brightness of a colour according to http://www.w3.org/TR/AERT#color-contrast
  121. * a return value of 0 corresponds to black
  122. * a return value of 255 corresponds to white
  123. * with all variations inbetween.
  124. *
  125. * @param string $colour hex triplet
  126. * @return integer 0 - 255
  127. */
  128. function brightness($colour) {
  129. list ($r, $g, $b)= ColourManipulator :: explode($colour);
  130. return ($r * 299 + $g * 587 + $b * 114) / 1000;
  131. }
  132. /**
  133. * Calculate the contrast between two colours according to http://www.w3.org/TR/AERT#color-contrast
  134. *
  135. * @param string $colour1 hex triplet
  136. * @param string $colour2 hex triplet
  137. * @return integer 0 - 765
  138. */
  139. function contrast($colour1, $colour2) {
  140. list ($r1, $g1, $b1)= ColourManipulator :: explode($colour1);
  141. list ($r2, $g2, $b2)= ColourManipulator :: explode($colour2);
  142. return (abs($r1 - $r2)) + (abs($g1 - $g2)) + (abs($b1 - $b2));
  143. }
  144. /**
  145. * Based upon a single colour, return a colour pallete
  146. *
  147. * The params array can have the following values and meanings:
  148. * shades: integer 0- 362 Determines the number of shades to return. The number of results may not match the
  149. * requested number of shades if duplicate colours are produced during the calculation, or a
  150. * calculated shade does not meet the minimum contrast requirement.
  151. * direction: string 'darken', 'lighten', 'both' or null. If the value is null the direction will be determined
  152. * to give the greatest contrast. e.g. passing #111111, will result in an array of colours which will
  153. * end with white (#FFFFFF). If the value 'both' is passed, the number of shades returned will be set
  154. * to the maximum, and a spectrum from black to white will be returned.
  155. * minContrast: Integer 0 - 255. If specified, the difference between calculated shades is checked and only if
  156. * the colour has a contrast greater the previuos shade in the sequence will it be added to the result
  157. * set.
  158. * maximize: boolean. maximize the passed shade before generating the pallette
  159. *
  160. * @param string $colour hex triplet
  161. * @param array $params see above for description
  162. * @return array an array of colour hex triplets
  163. */
  164. function pallette($colour, $params=array()) {
  165. $shades= isset ($params['shades']) ? $params['shades'] : 10;
  166. $direction= isset ($params['direction']) ? $params['direction'] : null;
  167. $minContrast= isset ($params['minContrast']) ? $params['minContrast'] : null;
  168. $maximize= isset ($params['maximize']) ? $params['maximize'] : true;
  169. if ($colour!='#000000'&&$maximize) {
  170. $colour= ColourManipulator :: maximize($colour);
  171. }
  172. if ($direction == 'both') {
  173. $params['shades']= 362; // Guess at maximum shades
  174. $params['direction']= 'lighten';
  175. $results= ColourManipulator :: pallette($colour, $params);
  176. $params['direction']= 'darken';
  177. $results= am($results, ColourManipulator :: pallette($colour, $params));
  178. $results= array_unique($results);
  179. sort($results);
  180. return $results;
  181. }
  182. list ($r, $g, $b)= ColourManipulator :: explode($colour);
  183. if (!$direction) {
  184. if (ColourManipulator :: brightness($colour) > 125) {
  185. // It's a bright colour, so darken
  186. $direction= 'darken';
  187. } else {
  188. // It's a dark colour, so lighten
  189. $direction= 'lighten';
  190. }
  191. }
  192. if ($direction == 'darken') {
  193. $colour2 = '#000000';
  194. } else {
  195. $colour2 = '#ffffff';
  196. }
  197. return ColourManipulator :: blend ($colour,$colour2,$params);
  198. }
  199. /**
  200. * Based upon a two colours, return a colour pallete from one colour to the other
  201. *
  202. * The params array can have the following values and meanings:
  203. * shades: integer 0- 362 Determines the number of shades to return. The number of results may not match the
  204. * requested number of shades if duplicate colours are produced during the calculation, or a
  205. * calculated shade does not meet the minimum contrast requirement.
  206. * minContrast: Integer 0 - 255. If specified, the difference between calculated shades is checked and only if
  207. * the colour has a contrast greater the previous shade in the sequence will it be added to the result
  208. * set.
  209. *
  210. * @param string $colour hex triplet
  211. * @param array $params see above for description
  212. * @return array an array of colour hex triplets
  213. */
  214. function blend($colour1,$colour2, $params=array()) {
  215. $shades= isset ($params['shades']) ? $params['shades'] : 10;
  216. $minContrast= isset ($params['minContrast']) ? $params['minContrast'] : null;
  217. list ($r1, $g1, $b1)= ColourManipulator :: explode($colour1);
  218. list ($r2, $g2, $b2)= ColourManipulator :: explode($colour2);
  219. $r_part= - ($r1-$r2) / $shades;
  220. $g_part= - ($g1-$g2) / $shades;
  221. $b_part= - ($b1-$b2) / $shades;
  222. if ($minContrast) {
  223. $comparison= $colour1;
  224. }
  225. $results[]= $colour1;
  226. for ($i= 1; $i <= $shades; $i++) {
  227. $shade= ColourManipulator :: implode(($r1 + $i * $r_part), ($g1 + $i * $g_part), ($b1 + $i * $b_part));
  228. if ($minContrast) {
  229. if (ColourManipulator :: contrast($comparison, $shade) >= $minContrast) {
  230. $comparison= $shade;
  231. if (!in_array($shade, $results)) {
  232. $results[]= $shade;
  233. }
  234. }
  235. } else {
  236. if (!in_array($shade, $results)) {
  237. $results[]= $shade;
  238. }
  239. }
  240. }
  241. if (!in_array(low($colour2), $results)) {
  242. $results[]= $colour2;
  243. }
  244. return $results;
  245. }
  246. }
  247. ?>

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.

3 Responses to Colour Contrast

  1. 1
    Nice. Smart :)
  2. 2
    Hi, PHP complain about the low() function in your class ? Could you provide this function ? Keep the great work
  3. 3

    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