|null */ private static ?array $rgb = null; /** @var string[]|null the 32 colormaps */ private static ?array $maps = null; private static ?string $identity = null; /** @return array */ public static function rgb(): array { if (self::$rgb === null) { $colors = []; foreach (self::RAMPS as $ramp => [$r, $g, $b]) { for ($s = 0; $s < 16; $s++) { $t = (($s + 1) / 16) ** 1.15; $white = $s > 12 ? ($s - 12) * 0.07 : 0.0; $c = []; foreach ([$r, $g, $b] as $v) { $x = $v * $t; $c[] = (int)max(0, min(255, round($x + (255 - $x) * $white))); } $colors[$ramp * 16 + $s] = $c; } } $colors[0] = [0, 0, 0]; self::$rgb = $colors; } return self::$rgb; } public static function index(int $ramp, int $shade): int { $i = $ramp * 16 + max(0, min(15, $shade)); return $i === self::TRANSPARENT ? 254 : $i; } /** The colour of a palette index darkened to a light level (0 bright .. 31 dark). */ public static function shade(int $index, int $level): int { if ($index === self::TRANSPARENT) { return $index; } $f = 1.0 - $level / self::LEVELS; $s = (int)round((($index & 15) + 1) * $f) - 1; return $s < 0 ? 0 : ($index & 0xF0) + $s; } /** Colormap $level as a 256-byte string (for strtr() and lookups). */ public static function colormap(int $level): string { if (self::$maps === null) { self::$maps = []; for ($l = 0; $l < self::LEVELS; $l++) { $m = ''; for ($i = 0; $i < 256; $i++) { $m .= chr(self::shade($i, $l)); } self::$maps[$l] = $m; } } return self::$maps[max(0, min(self::LEVELS - 1, $level))]; } /** "\x00\x01...\xFF": the identity map, first argument of strtr(). */ public static function identity(): string { return self::$identity ??= implode('', array_map('chr', range(0, 255))); } /** A picture's pixels at a light level (strtr: done in C, fast). */ public static function darken(string $pixels, int $level): string { return $level <= 0 ? $pixels : strtr($pixels, self::identity(), self::colormap($level)); } /** * index char => RGBA bytes, for strtr($frame, $map): the whole 8-bit frame to RGBA in one C call. * $tint = [r, g, b, amount 0..1]: the damage / pickup flash mixed into every colour. * @return array */ public static function rgbaMap(?array $tint = null): array { $map = []; foreach (self::rgb() as $i => [$r, $g, $b]) { if ($tint !== null) { [$tr, $tg, $tb, $a] = $tint; $r = (int)($r + ($tr - $r) * $a); $g = (int)($g + ($tg - $g) * $a); $b = (int)($b + ($tb - $b) * $a); } $map[chr($i)] = chr($r) . chr($g) . chr($b) . "\xFF"; } return $map; } /** The nearest palette index of an RGB colour (tools, slow). */ public static function nearest(int $r, int $g, int $b): int { $best = 0; $bestD = PHP_INT_MAX; foreach (self::rgb() as $i => [$pr, $pg, $pb]) { if ($i === self::TRANSPARENT) { continue; } $d = ($pr - $r) ** 2 * 3 + ($pg - $g) ** 2 * 4 + ($pb - $b) ** 2 * 2; if ($d < $bestD) { $bestD = $d; $best = $i; } } return $best; } }