Skip to content

Support alpha in hexadecimal colors #3052

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion webapp/src/Entity/ContestProblem.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ public function validate(ExecutionContextInterface $context): void
{
if ($this->getColor() && Utils::convertToHex($this->getColor()) === null) {
$context
->buildViolation('This is not a valid color')
->buildViolation('This is not a valid color.')
->atPath('color')
->addViolation();
}
Expand Down
56 changes: 3 additions & 53 deletions webapp/src/Twig/TwigExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -1155,64 +1155,14 @@
return 'fas fa-file-' . $iconName;
}

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
{
$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);

// 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');
$rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff');

Check failure on line 1160 in webapp/src/Twig/TwigExtension.php

View workflow job for this annotation

GitHub Actions / phpcs

Line indented incorrectly; expected at least 8 spaces, found 4

Check failure on line 1160 in webapp/src/Twig/TwigExtension.php

View workflow job for this annotation

GitHub Actions / phpcs

Spaces must be used to indent lines; tabs are not allowed
if ($grayedOut || empty($rgb)) {
$rgb = Utils::convertToHex('whitesmoke');
}

[$foreground, $border] = $this->hexToForegroundAndBorder($rgb);
[$foreground, $border] = Utils::hexToForegroundAndBorder($rgb);

if ($grayedOut) {
$foreground = 'silver';
Expand All @@ -1238,7 +1188,7 @@
$rgb = Utils::convertToHex('whitesmoke');
}

[$foreground, $border] = $this->hexToForegroundAndBorder($rgb);
[$foreground, $border] = Utils::hexToForegroundAndBorder($rgb);

if (!$matrixItem->isCorrect) {
$foreground = 'silver';
Expand Down
95 changes: 89 additions & 6 deletions webapp/src/Utils/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -343,19 +343,25 @@
}

/**
* Parse a hex color into it's three RGB values.
* Parse a hex color into it's four RGBA values.
*
* @return array{int, int, int}
* @return array{int, int, int, int}
*/
public static function parseHexColor(string $hex): array
{
$tmp = substr($hex, 1);
if (strlen($tmp) % 3 == 0) {
$mult = strlen($tmp) / 3;
$hex .= str_repeat("f", $mult);
}
// Source: https://stackoverflow.com/a/21966100
$length = (strlen($hex) - 1) / 3;
$length = (strlen($hex) - 1) / 4;
$fact = [17, 1, 0.062272][$length - 1];
return [
(int)round(hexdec(substr($hex, 1, $length)) * $fact),
(int)round(hexdec(substr($hex, 1 + $length, $length)) * $fact),
(int)round(hexdec(substr($hex, 1 + 2 * $length, $length)) * $fact)
(int)round(hexdec(substr($hex, 1 + 2 * $length, $length)) * $fact),
(int)round(hexdec(substr($hex, 1 + 3 * $length, $length)) * $fact)
];
}

Expand All @@ -371,11 +377,88 @@
/**
* Convert an RGB triple into a CSS hex color.
*
* @param array{int, int, int} $color
* @param array{int, int, int}|array{int, int, int, int} $color
*/
public static function rgbToHex(array $color): string
{
return "#" . static::componentToHex($color[0]) . static::componentToHex($color[1]) . static::componentToHex($color[2]);
$result = "#";
if (count($color) === 3) {
$color[] = 255;
}

Check failure on line 387 in webapp/src/Utils/Utils.php

View workflow job for this annotation

GitHub Actions / phpcs

Closing brace indented incorrectly; expected 8 spaces, found 4

Check failure on line 387 in webapp/src/Utils/Utils.php

View workflow job for this annotation

GitHub Actions / phpcs

Line indented incorrectly; expected 8 spaces, found 4

Check failure on line 387 in webapp/src/Utils/Utils.php

View workflow job for this annotation

GitHub Actions / phpcs

Spaces must be used to indent lines; tabs are not allowed
for ($i=0; $i<count($color); $i++) {
$result .= static::componentToHex($color[$i]);
}

Check failure on line 390 in webapp/src/Utils/Utils.php

View workflow job for this annotation

GitHub Actions / phpcs

Closing brace indented incorrectly; expected 8 spaces, found 4

Check failure on line 390 in webapp/src/Utils/Utils.php

View workflow job for this annotation

GitHub Actions / phpcs

Line indented incorrectly; expected 8 spaces, found 4

Check failure on line 390 in webapp/src/Utils/Utils.php

View workflow job for this annotation

GitHub Actions / phpcs

Spaces must be used to indent lines; tabs are not allowed
return $result;
}

/**
* @param array{int, int, int}|array{int, int, int, int} $rgba
*
* @return array{int, int, int}
*/
public static function blendAlphaBackground(array $rgba, string $bg): array
{
if (count($rgba) === 3) {
return $rgba;
}

Check failure on line 403 in webapp/src/Utils/Utils.php

View workflow job for this annotation

GitHub Actions / phpcs

Line indented incorrectly; expected 8 spaces, found 4

Check failure on line 403 in webapp/src/Utils/Utils.php

View workflow job for this annotation

GitHub Actions / phpcs

Spaces must be used to indent lines; tabs are not allowed
$result = [];
$bg = static::parseHexColor($bg);
for ($i=0; $i<3; $i++) {
$result[] = $rgba[$i] * ($rgba[3]/255) + $bg[$i] * (1 - ($rgba[3]/255));
}
return $result;
}

public static function relativeLuminance(string $hexRGB, string $rgb_background = "#fff"): float
{
// See https://en.wikipedia.org/wiki/Relative_luminance
$rgba = static::parseHexColor($hexRGB);
[$r, $g, $b] = static::blendAlphaBackground($rgba, $rgb_background);

[$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;
}

public static function apcaContrast(string $fgColor, string $bgColor): float
{
// Based on WCAG 3.x (https://www.w3.org/TR/wcag-3.0/)
$luminanceForeground = static::relativeLuminance($fgColor);
$luminanceBackground = static::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}
*/
public static function hexToForegroundAndBorder(string $rgb): array
{
$background = Utils::parseHexColor($rgb);

// Pick a border that's a bit darker.
// We explicit keep the alpha channel as-is.
$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);

// Pick the text color with the biggest absolute contrast.
$contrastWithWhite = static::apcaContrast('#ffffff', $rgb);
$contrastWithBlack = static::apcaContrast('#000000', $rgb);

$foreground = (abs($contrastWithBlack) > abs($contrastWithWhite)) ? '#000000' : '#ffffff';

return [$foreground, $border];
}

/**
Expand Down
53 changes: 45 additions & 8 deletions webapp/tests/Unit/Utils/UtilsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,13 @@ public function testConvertToHexConvert(): void

public function testParseHexColor(): void
{
self::assertEquals([255, 255, 255], Utils::parseHexColor('#ffffff'));
self::assertEquals([0, 0, 0], Utils::parseHexColor('#000000'));
self::assertEquals([171, 205, 239], Utils::parseHexColor('#abcdef'));
self::assertEquals([254, 220, 186], Utils::parseHexColor('#FEDCBA'));
self::assertEquals([255, 255, 255, 255], Utils::parseHexColor('#ffffff'));
self::assertEquals([0, 0, 0, 255], Utils::parseHexColor('#000000'));
self::assertEquals([0, 0, 0, 255], Utils::parseHexColor('#000'));
self::assertEquals([0, 0, 0, 0], Utils::parseHexColor('#00000000'));
self::assertEquals([0, 0, 0, 0], Utils::parseHexColor('#0000'));
self::assertEquals([171, 205, 239, 255], Utils::parseHexColor('#abcdef'));
self::assertEquals([254, 220, 186, 255], Utils::parseHexColor('#FEDCBA'));
}

public function testComponentToHex(): void
Expand All @@ -337,10 +340,44 @@ public function testComponentToHex(): void

public function testRgbToHex(): void
{
self::assertEquals('#ffffff', Utils::rgbToHex([255, 255, 255]));
self::assertEquals('#000000', Utils::rgbToHex([0, 0, 0]));
self::assertEquals('#abcdef', Utils::rgbToHex([171, 205, 239]));
self::assertEquals('#fedcba', Utils::rgbToHex([254, 220, 186]));
self::assertEquals('#ffffffff', Utils::rgbToHex([255, 255, 255, 255]));
self::assertEquals('#ffffff00', Utils::rgbToHex([255, 255, 255, 0]));
self::assertEquals('#000000ff', Utils::rgbToHex([0, 0, 0, 255]));
self::assertEquals('#00000000', Utils::rgbToHex([0, 0, 0, 0]));
self::assertEquals('#abcdefff', Utils::rgbToHex([171, 205, 239, 255]));
self::assertEquals('#fedcbaff', Utils::rgbToHex([254, 220, 186, 255]));
}

public function testRelativeLuminance(): void
{
self::assertEquals(0.0, Utils::relativeLuminance("#000000"));
self::assertEquals(1.0, Utils::relativeLuminance("#FFFfff"));
self::assertEquals(1.0, Utils::relativeLuminance("#FFFfffFF"));
self::assertEquals(0.00751604342389449, Utils::relativeLuminance("#123"));
self::assertEquals(0.528186803960141, Utils::relativeLuminance("#1234"));
}

/**
* Test that the APCA contrast function returns the correct data
*/
public function testApcaContrast(): void
{
self::assertEquals(-114.0, Utils::apcaContrast("#ffffff", "#000000"));
self::assertEquals(114.0, Utils::apcaContrast("#000000", "#ffffff"));
self::assertEquals(0.0, Utils::apcaContrast("#fffFFF", "#FFFfff"));
self::assertEquals(-0.36, Utils::apcaContrast("#111", "#111"));
self::assertEquals(58.09, Utils::apcaContrast("#123f", "#975A"));
self::assertEquals(25.15, Utils::apcaContrast("#11223344", "#00110011"));
self::assertEquals(-35.06, Utils::apcaContrast("#11223344", "#FF0011"));
}

public function testHexToForegroundAndBorder(): void
{
self::assertEquals(["#000000", "#bfbd9dff"], Utils::hexToForegroundAndBorder("#fffDDD"));
self::assertEquals(["#ffffff", "#000000ff"], Utils::hexToForegroundAndBorder("#000000"));
self::assertEquals(["#000000", "#6a7b8cff"], Utils::hexToForegroundAndBorder("#ABC"));
self::assertEquals(["#ffffff", "#00000099"], Utils::hexToForegroundAndBorder("#1239"));
self::assertEquals(["#000000", "#00000040"], Utils::hexToForegroundAndBorder("#10203040"));
}

/**
Expand Down
Loading