Skip to content

Commit f4e085c

Browse files
committed
Allow Alpha in RGBA strings
The Alpha is allowed by the CLICS spec but we don't really use those in the UI normally. The only place where we have to fully ignore the alpha is for the luminance. In that case we now by default merge it with a white background most likely this will be the same for either background but this was not tested. As bonus we now also test for the shorthand (#ABC) versuse (#AABBCC). The functions using the strings have moved from the TwigExtension to utils as the they don't really depend on Twig itself. This makes testing easier and can use this in the future for other elements.
1 parent 9dbbb93 commit f4e085c

File tree

3 files changed

+137
-67
lines changed

3 files changed

+137
-67
lines changed

webapp/src/Twig/TwigExtension.php

Lines changed: 3 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,64 +1155,14 @@ public function fileTypeIcon(string $type): string
11551155
return 'fas fa-file-' . $iconName;
11561156
}
11571157

1158-
private function relativeLuminance(string $rgb): float
1159-
{
1160-
// See https://en.wikipedia.org/wiki/Relative_luminance
1161-
[$r, $g, $b] = Utils::parseHexColor($rgb);
1162-
1163-
[$lr, $lg, $lb] = [
1164-
pow($r / 255, 2.4),
1165-
pow($g / 255, 2.4),
1166-
pow($b / 255, 2.4),
1167-
];
1168-
1169-
return 0.2126 * $lr + 0.7152 * $lg + 0.0722 * $lb;
1170-
}
1171-
1172-
private function apcaContrast(string $fgColor, string $bgColor): float
1173-
{
1174-
// Based on WCAG 3.x (https://www.w3.org/TR/wcag-3.0/)
1175-
$luminanceForeground = $this->relativeLuminance($fgColor);
1176-
$luminanceBackground = $this->relativeLuminance($bgColor);
1177-
1178-
$contrast = ($luminanceBackground > $luminanceForeground)
1179-
? (pow($luminanceBackground, 0.56) - pow($luminanceForeground, 0.57)) * 1.14
1180-
: (pow($luminanceBackground, 0.65) - pow($luminanceForeground, 0.62)) * 1.14;
1181-
1182-
return round($contrast * 100, 2);
1183-
}
1184-
1185-
/**
1186-
* @return array{string, string}
1187-
*/
1188-
private function hexToForegroundAndBorder(string $rgb): array
1189-
{
1190-
$background = Utils::parseHexColor($rgb);
1191-
1192-
// Pick a border that's a bit darker.
1193-
$darker = $background;
1194-
$darker[0] = max($darker[0] - 64, 0);
1195-
$darker[1] = max($darker[1] - 64, 0);
1196-
$darker[2] = max($darker[2] - 64, 0);
1197-
$border = Utils::rgbToHex($darker);
1198-
1199-
// Pick the text color with the biggest absolute contrast.
1200-
$contrastWithWhite = $this->apcaContrast('#ffffff', $rgb);
1201-
$contrastWithBlack = $this->apcaContrast('#000000', $rgb);
1202-
1203-
$foreground = (abs($contrastWithBlack) > abs($contrastWithWhite)) ? '#000000' : '#ffffff';
1204-
1205-
return [$foreground, $border];
1206-
}
1207-
12081158
public function problemBadge(ContestProblem $problem, bool $grayedOut = false): string
12091159
{
1210-
$rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff');
1160+
$rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff');
12111161
if ($grayedOut || empty($rgb)) {
12121162
$rgb = Utils::convertToHex('whitesmoke');
12131163
}
12141164

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

12171167
if ($grayedOut) {
12181168
$foreground = 'silver';
@@ -1238,7 +1188,7 @@ public function problemBadgeMaybe(
12381188
$rgb = Utils::convertToHex('whitesmoke');
12391189
}
12401190

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

12431193
if (!$matrixItem->isCorrect) {
12441194
$foreground = 'silver';

webapp/src/Utils/Utils.php

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -343,19 +343,25 @@ public static function convertToColor(string $hex): ?string
343343
}
344344

345345
/**
346-
* Parse a hex color into it's three RGB values.
346+
* Parse a hex color into it's four RGBA values.
347347
*
348-
* @return array{int, int, int}
348+
* @return array{int, int, int, int}
349349
*/
350350
public static function parseHexColor(string $hex): array
351351
{
352+
$tmp = substr($hex, 1);
353+
if (strlen($tmp) % 3 == 0) {
354+
$mult = strlen($tmp) / 3;
355+
$hex .= str_repeat("f", $mult);
356+
}
352357
// Source: https://stackoverflow.com/a/21966100
353-
$length = (strlen($hex) - 1) / 3;
358+
$length = (strlen($hex) - 1) / 4;
354359
$fact = [17, 1, 0.062272][$length - 1];
355360
return [
356361
(int)round(hexdec(substr($hex, 1, $length)) * $fact),
357362
(int)round(hexdec(substr($hex, 1 + $length, $length)) * $fact),
358-
(int)round(hexdec(substr($hex, 1 + 2 * $length, $length)) * $fact)
363+
(int)round(hexdec(substr($hex, 1 + 2 * $length, $length)) * $fact),
364+
(int)round(hexdec(substr($hex, 1 + 3 * $length, $length)) * $fact)
359365
];
360366
}
361367

@@ -371,11 +377,88 @@ public static function componentToHex(int $component): string
371377
/**
372378
* Convert an RGB triple into a CSS hex color.
373379
*
374-
* @param array{int, int, int} $color
380+
* @param array{int, int, int}|array{int, int, int, int} $color
375381
*/
376382
public static function rgbToHex(array $color): string
377383
{
378-
return "#" . static::componentToHex($color[0]) . static::componentToHex($color[1]) . static::componentToHex($color[2]);
384+
$result = "#";
385+
if (count($color) === 3) {
386+
$color[] = 255;
387+
}
388+
for ($i=0; $i<count($color); $i++) {
389+
$result .= static::componentToHex($color[$i]);
390+
}
391+
return $result;
392+
}
393+
394+
/**
395+
* @param array{int, int, int}|array{int, int, int, int} $rgba
396+
*
397+
* @return array{int, int, int}
398+
*/
399+
public static function blendAlphaBackground(array $rgba, string $bg): array
400+
{
401+
if (count($rgba) === 3) {
402+
return $rgba;
403+
}
404+
$result = [];
405+
$bg = static::parseHexColor($bg);
406+
for ($i=0; $i<3; $i++) {
407+
$result[] = $rgba[$i] * ($rgba[3]/255) + $bg[$i] * (1 - ($rgba[3]/255));
408+
}
409+
return $result;
410+
}
411+
412+
public static function relativeLuminance(string $hexRGB, string $rgb_background = "#fff"): float
413+
{
414+
// See https://en.wikipedia.org/wiki/Relative_luminance
415+
$rgba = static::parseHexColor($hexRGB);
416+
[$r, $g, $b] = static::blendAlphaBackground($rgba, $rgb_background);
417+
418+
[$lr, $lg, $lb] = [
419+
pow($r / 255, 2.4),
420+
pow($g / 255, 2.4),
421+
pow($b / 255, 2.4),
422+
];
423+
424+
return 0.2126 * $lr + 0.7152 * $lg + 0.0722 * $lb;
425+
}
426+
427+
public static function apcaContrast(string $fgColor, string $bgColor): float
428+
{
429+
// Based on WCAG 3.x (https://www.w3.org/TR/wcag-3.0/)
430+
$luminanceForeground = static::relativeLuminance($fgColor);
431+
$luminanceBackground = static::relativeLuminance($bgColor);
432+
433+
$contrast = ($luminanceBackground > $luminanceForeground)
434+
? (pow($luminanceBackground, 0.56) - pow($luminanceForeground, 0.57)) * 1.14
435+
: (pow($luminanceBackground, 0.65) - pow($luminanceForeground, 0.62)) * 1.14;
436+
437+
return round($contrast * 100, 2);
438+
}
439+
440+
/**
441+
* @return array{string, string}
442+
*/
443+
public static function hexToForegroundAndBorder(string $rgb): array
444+
{
445+
$background = Utils::parseHexColor($rgb);
446+
447+
// Pick a border that's a bit darker.
448+
// We explicit keep the alpha channel as-is.
449+
$darker = $background;
450+
$darker[0] = max($darker[0] - 64, 0);
451+
$darker[1] = max($darker[1] - 64, 0);
452+
$darker[2] = max($darker[2] - 64, 0);
453+
$border = Utils::rgbToHex($darker);
454+
455+
// Pick the text color with the biggest absolute contrast.
456+
$contrastWithWhite = static::apcaContrast('#ffffff', $rgb);
457+
$contrastWithBlack = static::apcaContrast('#000000', $rgb);
458+
459+
$foreground = (abs($contrastWithBlack) > abs($contrastWithWhite)) ? '#000000' : '#ffffff';
460+
461+
return [$foreground, $border];
379462
}
380463

381464
/**

webapp/tests/Unit/Utils/UtilsTest.php

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -321,10 +321,13 @@ public function testConvertToHexConvert(): void
321321

322322
public function testParseHexColor(): void
323323
{
324-
self::assertEquals([255, 255, 255], Utils::parseHexColor('#ffffff'));
325-
self::assertEquals([0, 0, 0], Utils::parseHexColor('#000000'));
326-
self::assertEquals([171, 205, 239], Utils::parseHexColor('#abcdef'));
327-
self::assertEquals([254, 220, 186], Utils::parseHexColor('#FEDCBA'));
324+
self::assertEquals([255, 255, 255, 255], Utils::parseHexColor('#ffffff'));
325+
self::assertEquals([0, 0, 0, 255], Utils::parseHexColor('#000000'));
326+
self::assertEquals([0, 0, 0, 255], Utils::parseHexColor('#000'));
327+
self::assertEquals([0, 0, 0, 0], Utils::parseHexColor('#00000000'));
328+
self::assertEquals([0, 0, 0, 0], Utils::parseHexColor('#0000'));
329+
self::assertEquals([171, 205, 239, 255], Utils::parseHexColor('#abcdef'));
330+
self::assertEquals([254, 220, 186, 255], Utils::parseHexColor('#FEDCBA'));
328331
}
329332

330333
public function testComponentToHex(): void
@@ -337,10 +340,44 @@ public function testComponentToHex(): void
337340

338341
public function testRgbToHex(): void
339342
{
340-
self::assertEquals('#ffffff', Utils::rgbToHex([255, 255, 255]));
341-
self::assertEquals('#000000', Utils::rgbToHex([0, 0, 0]));
342-
self::assertEquals('#abcdef', Utils::rgbToHex([171, 205, 239]));
343-
self::assertEquals('#fedcba', Utils::rgbToHex([254, 220, 186]));
343+
self::assertEquals('#ffffffff', Utils::rgbToHex([255, 255, 255, 255]));
344+
self::assertEquals('#ffffff00', Utils::rgbToHex([255, 255, 255, 0]));
345+
self::assertEquals('#000000ff', Utils::rgbToHex([0, 0, 0, 255]));
346+
self::assertEquals('#00000000', Utils::rgbToHex([0, 0, 0, 0]));
347+
self::assertEquals('#abcdefff', Utils::rgbToHex([171, 205, 239, 255]));
348+
self::assertEquals('#fedcbaff', Utils::rgbToHex([254, 220, 186, 255]));
349+
}
350+
351+
public function testRelativeLuminance(): void
352+
{
353+
self::assertEquals(0.0, Utils::relativeLuminance("#000000"));
354+
self::assertEquals(1.0, Utils::relativeLuminance("#FFFfff"));
355+
self::assertEquals(1.0, Utils::relativeLuminance("#FFFfffFF"));
356+
self::assertEquals(0.00751604342389449, Utils::relativeLuminance("#123"));
357+
self::assertEquals(0.528186803960141, Utils::relativeLuminance("#1234"));
358+
}
359+
360+
/**
361+
* Test that the APCA contrast function returns the correct data
362+
*/
363+
public function testApcaContrast(): void
364+
{
365+
self::assertEquals(-114.0, Utils::apcaContrast("#ffffff", "#000000"));
366+
self::assertEquals(114.0, Utils::apcaContrast("#000000", "#ffffff"));
367+
self::assertEquals(0.0, Utils::apcaContrast("#fffFFF", "#FFFfff"));
368+
self::assertEquals(-0.36, Utils::apcaContrast("#111", "#111"));
369+
self::assertEquals(58.09, Utils::apcaContrast("#123f", "#975A"));
370+
self::assertEquals(25.15, Utils::apcaContrast("#11223344", "#00110011"));
371+
self::assertEquals(-35.06, Utils::apcaContrast("#11223344", "#FF0011"));
372+
}
373+
374+
public function testHexToForegroundAndBorder(): void
375+
{
376+
self::assertEquals(["#000000", "#bfbd9dff"], Utils::hexToForegroundAndBorder("#fffDDD"));
377+
self::assertEquals(["#ffffff", "#000000ff"], Utils::hexToForegroundAndBorder("#000000"));
378+
self::assertEquals(["#000000", "#6a7b8cff"], Utils::hexToForegroundAndBorder("#ABC"));
379+
self::assertEquals(["#ffffff", "#00000099"], Utils::hexToForegroundAndBorder("#1239"));
380+
self::assertEquals(["#000000", "#00000040"], Utils::hexToForegroundAndBorder("#10203040"));
344381
}
345382

346383
/**

0 commit comments

Comments
 (0)