| @@ -201,8 +201,82 @@ | ||
| 201 | 201 | return self::contrast($background, $light) >= self::contrast($background, $dark) ? $light : $dark; |
| 202 | 202 | } |
| 203 | 203 | |
| 204 | 204 | /** |
| 205 | + * A text colour that reads on a background (WCAG 4.5:1) wherever one can. | |
| 206 | + * | |
| 207 | + * readableOn()'s usual pair is kept whenever it reads, so every partner it | |
| 208 | + * already made readable is unchanged. On a mid-tone background neither | |
| 209 | + * half of that pair reaches 4.5:1 (#809400 peaks at 4.30:1 with #1f2937); | |
| 210 | + * pure black or white — whichever reads better — goes further, and is | |
| 211 | + * taken only when it does. | |
| 212 | + * | |
| 213 | + * @param string $background | |
| 214 | + * @return string | |
| 215 | + */ | |
| 216 | + public static function readableText($background): string | |
| 217 | + { | |
| 218 | + $picked = self::readableOn($background); | |
| 219 | + $pickedContrast = self::contrast($background, $picked); | |
| 220 | + | |
| 221 | + if ($pickedContrast >= 4.5) { | |
| 222 | + return $picked; | |
| 223 | + } | |
| 224 | + | |
| 225 | + $extreme = self::contrast($background, '#ffffff') >= self::contrast($background, '#000000') | |
| 226 | + ? '#ffffff' | |
| 227 | + : '#000000'; | |
| 228 | + | |
| 229 | + return self::contrast($background, $extreme) > $pickedContrast ? $extreme : $picked; | |
| 230 | + } | |
| 231 | + | |
| 232 | + /** | |
| 233 | + * Mix text into a surface, keeping as little of the text as still reads. | |
| 234 | + * | |
| 235 | + * Starts at $percent of the text and moves toward it in small steps until | |
| 236 | + * the mix reaches $minimum contrast on the surface — a mix that already | |
| 237 | + * reads is returned unchanged. Text that does not read on the surface | |
| 238 | + * itself comes back as the text: nothing between the two reads better. | |
| 239 | + * | |
| 240 | + * @param string $text | |
| 241 | + * @param string $surface | |
| 242 | + * @param int $percent Starting share of the text, 0-100. | |
| 243 | + * @param float $minimum | |
| 244 | + * @return string Hex, or '' when either colour is unreadable. | |
| 245 | + */ | |
| 246 | + public static function readableMix($text, $surface, int $percent, float $minimum = 4.5): string | |
| 247 | + { | |
| 248 | + if (!self::parse($text) || !self::parse($surface)) { | |
| 249 | + return ''; | |
| 250 | + } | |
| 251 | + | |
| 252 | + for ($share = max(0, $percent); $share < 100; $share += 2) { | |
| 253 | + $mixed = self::mix($text, $surface, $share); | |
| 254 | + | |
| 255 | + if (self::contrast($surface, $mixed) >= $minimum) { | |
| 256 | + return $mixed; | |
| 257 | + } | |
| 258 | + } | |
| 259 | + | |
| 260 | + return self::hex($text); | |
| 261 | + } | |
| 262 | + | |
| 263 | + /** | |
| 264 | + * Move a colour away from itself — lighter when it is dark, darker when it | |
| 265 | + * is light. How a hover is derived from a button that states none. | |
| 266 | + * | |
| 267 | + * @param string $color | |
| 268 | + * @param int $percent | |
| 269 | + * @return string | |
| 270 | + */ | |
| 271 | + public static function shiftFromItself($color, int $percent = 12): string | |
| 272 | + { | |
| 273 | + return self::isDark($color) | |
| 274 | + ? self::lighten($color, $percent) | |
| 275 | + : self::darken($color, $percent); | |
| 276 | + } | |
| 277 | + | |
| 278 | + /** | |
| 205 | 279 | * Nudge a colour toward white. |
| 206 | 280 | * |
| 207 | 281 | * @param string $color |
| 208 | 282 | * @param int $percent |