From 0c5ae9050d12b534ba7e5be5c7a1d9ca222c6544 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Fri, 28 Feb 2025 19:37:09 +0000 Subject: [PATCH] Fix contrast color calculation --- webapp/src/Twig/TwigExtension.php | 66 +++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index af34342b4c..0155f53e9f 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -1145,12 +1145,38 @@ public function fileTypeIcon(string $type): string return 'fas fa-file-' . $iconName; } - public function problemBadge(ContestProblem $problem, bool $grayedOut = false): string + private function relativeLuminance(string $rgb): float + { + // See https://en.wikipedia.org/wiki/Relative_luminance + [$r, $g, $b] = Utils::parseHexColor($rgb); + + [$lr, $lg, $lb] = [ + pow($r / 255, 2.4), + pow($g / 255, 2.4), + pow($b / 255, 2.4), + ]; + + return 0.2126 * $lr + 0.7152 * $lg + 0.0722 * $lb; + } + + private function apcaContrast(string $fgColor, string $bgColor): float + { + // Based on WCAG 3.x (https://www.w3.org/TR/wcag-3.0/) + $luminanceForeground = $this->relativeLuminance($fgColor); + $luminanceBackground = $this->relativeLuminance($bgColor); + + $contrast = ($luminanceBackground > $luminanceForeground) + ? (pow($luminanceBackground, 0.56) - pow($luminanceForeground, 0.57)) * 1.14 + : (pow($luminanceBackground, 0.65) - pow($luminanceForeground, 0.62)) * 1.14; + + return round($contrast * 100, 2); + } + + /** + * @return array{string, string} + */ + private function hexToForegroundAndBorder(string $rgb): array { - $rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff'); - if ($grayedOut || empty($rgb)) { - $rgb = Utils::convertToHex('whitesmoke'); - } $background = Utils::parseHexColor($rgb); // Pick a border that's a bit darker. @@ -1160,8 +1186,24 @@ public function problemBadge(ContestProblem $problem, bool $grayedOut = false): $darker[2] = max($darker[2] - 64, 0); $border = Utils::rgbToHex($darker); - // Pick the foreground text color based on the background color. - $foreground = ($background[0] + $background[1] + $background[2] > 450) ? '#000000' : '#ffffff'; + // Pick the text color with the biggest absolute contrast. + $contrastWithWhite = $this->apcaContrast('#ffffff', $rgb); + $contrastWithBlack = $this->apcaContrast('#000000', $rgb); + + $foreground = (abs($contrastWithBlack) > abs($contrastWithWhite)) ? '#000000' : '#ffffff'; + + return [$foreground, $border]; + } + + public function problemBadge(ContestProblem $problem, bool $grayedOut = false): string + { + $rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff'); + if ($grayedOut || empty($rgb)) { + $rgb = Utils::convertToHex('whitesmoke'); + } + + [$foreground, $border] = $this->hexToForegroundAndBorder($rgb); + if ($grayedOut) { $foreground = 'silver'; $border = 'linen'; @@ -1181,17 +1223,9 @@ public function problemBadgeMaybe(ContestProblem $problem, ScoreboardMatrixItem if (!$matrixItem->isCorrect || empty($rgb)) { $rgb = Utils::convertToHex('whitesmoke'); } - $background = Utils::parseHexColor($rgb); - // Pick a border that's a bit darker. - $darker = $background; - $darker[0] = max($darker[0] - 64, 0); - $darker[1] = max($darker[1] - 64, 0); - $darker[2] = max($darker[2] - 64, 0); - $border = Utils::rgbToHex($darker); + [$foreground, $border] = $this->hexToForegroundAndBorder($rgb); - // Pick the foreground text color based on the background color. - $foreground = ($background[0] + $background[1] + $background[2] > 450) ? '#000000' : '#ffffff'; if (!$matrixItem->isCorrect) { $foreground = 'silver'; $border = 'linen';