<?phpnamespace App\Gui;use JumPHP\Renderer\Renderer;use JumPHP\Renderer\Sdl;/** * Draws the applet's smileys. They were not pictures but drawing code (net.spray.chat.a.g): rectangles, lines and * ovals per head (the nose), eyes, mouth and hands, the face filled with the chosen colour. That code was turned * into data (assets/smileys/ops.json); each smiley met is drawn once into a picture (GD) and kept as a texture. */final class SmileyPainter{ public const HEIGHT = 22; /** u[] of the applet: the colours of a face (0..9, 10 = the default yellow). */ public const FACE_COLORS = ['#00c0ff', '#ff2020', '#ff40c0', '#ffc040', '#c0c0c0', '#c0c0ff', '#c0ff40', '#40ff40', '#ffa0ff', '#ffffff', '#ffff80']; /** @var array<string, mixed>|null */ private static ?array $ops = null; /** @var array<string, bool> textures made (by key) */ private static array $made = []; /** The width in px (unscaled) of a smiley: 23 a face (+9 per hand), 29 a drawing, 32 a picture. */ public static function width(array $smiley): int { return match ($smiley['kind']) { 'face' => 23 + (($smiley['hands'] & 3) ? 9 : 0) + (($smiley['hands'] & 12) ? 9 : 0), 'big' => 29, default => 32, }; } public static function key(array $smiley): string { return $smiley['kind'] === 'face' ? sprintf('smiley-f-%d-%d-%d-%d-%d', $smiley['eyes'], $smiley['nose'], $smiley['mouth'], $smiley['color'], $smiley['hands']) : sprintf('smiley-%s-%d-%d', $smiley['kind'], $smiley['n'], $smiley['color'] ?? 0); } public static function draw(Renderer $r, ?Sdl $sdl, array $smiley, float $x, float $y, float $scale): void { if ($smiley['kind'] === 'image') { self::drawImage($r, $sdl, (int)$smiley['n'], $x, $y + $scale, $scale); // 20 px high in the 22 of a smiley return; } $w = self::width($smiley); $h = $smiley['kind'] === 'big' ? 24 : self::HEIGHT; if ($sdl === null || !function_exists('imagecreatetruecolor')) { $r->fillCircle($x + $w * $scale / 2, $y + self::HEIGHT * $scale / 2, 9 * $scale, self::FACE_COLORS[$smiley['color'] ?? 10] ?? '#ffff80'); return; } $key = self::key($smiley); if (!isset(self::$made[$key])) { $sdl->createTextureFromGd($key, self::image($smiley)); self::$made[$key] = true; } $r->copy($sdl->getTexture($key)['texture'], null, $x, $y, $w * $scale, $h * $scale); } public const SHEET = 'assets/smileys/smiley.png'; public const CELL_W = 32; public const CELL_H = 20; /** A picture of smiley.gif (20 a row, cells of 32 x 20), the applet's "@NN". */ public static function drawImage(Renderer $r, ?Sdl $sdl, int $n, float $x, float $y, float $scale): void { if ($sdl === null) { $r->fillRect($x, $y, self::CELL_W * $scale, self::CELL_H * $scale, '#ffff80'); return; } if (!$sdl->hasTexture('smiley-sheet')) { $sdl->loadTexture('smiley-sheet', self::SHEET); } $src = [($n % 20) * self::CELL_W, intdiv($n, 20) * self::CELL_H, self::CELL_W, self::CELL_H]; $r->copy($sdl->getTexture('smiley-sheet')['texture'], $src, $x, $y, self::CELL_W * $scale, self::CELL_H * $scale); } /** The smiley as a GD picture with a transparent background (also used by the tests). */ public static function image(array $smiley): \GdImage { $w = self::width($smiley); $h = $smiley['kind'] === 'big' ? 24 : self::HEIGHT; $img = imagecreatetruecolor($w, $h); imagealphablending($img, false); imagesavealpha($img, true); imagefill($img, 0, 0, imagecolorallocatealpha($img, 0, 0, 0, 127)); $face = self::FACE_COLORS[$smiley['color'] ?? 10] ?? self::FACE_COLORS[10]; foreach (self::operations($smiley) as $op) { self::apply($img, $op, $face); } return $img; } /** * The drawing operations of a smiley, in the applet's order: left hand, right hand, head, eyes, mouth. The eyes * and mouth are given for the round head and moved per head; a left hand moves everything 9 px right. * * @return list<array> */ public static function operations(array $smiley): array { $data = self::data(); if ($smiley['kind'] === 'big') { return $data['big'][sprintf('%02d', $smiley['n'])] ?? []; } if ($smiley['kind'] !== 'face') { return []; } $nose = (string)$smiley['nose']; $hands = (int)$smiley['hands']; $ops = []; if ($hands & 3) { $ops = [...$ops, ...($data['hands']['left'][(string)($hands & 3)][$nose] ?? [])]; } if ($hands & 12) { $ops = [...$ops, ...($data['hands']['right'][(string)($hands & 12)][$nose] ?? [])]; } $ops = [...$ops, ...($data['heads'][$nose] ?? [])]; [$fx, $fy] = $data['feature_offset']['per_nose'][$nose] ?? [0, 0]; if ((int)$smiley['mouth'] === 18) { $fy += (int)($data['feature_offset']['mouth18_extra_dy'] ?? -1); } foreach ([...($data['eyes'][(string)$smiley['eyes']] ?? []), ...($data['mouths'][(string)$smiley['mouth']] ?? [])] as $op) { $ops[] = self::shift($op, (int)$fx, (int)$fy); } $faceX = ($hands & 3) ? 9 : 0; return $faceX === 0 ? $ops : array_map(static fn (array $op) => self::shift($op, $faceX, 0), $ops); } private static function shift(array $op, int $dx, int $dy): array { $op[1] += $dx; $op[2] += $dy; if ($op[0] === 'line') { $op[3] += $dx; $op[4] += $dy; } return $op; } private static function apply(\GdImage $img, array $op, string $face): void { [$kind, $a, $b, $c, $d, $color] = $op + [null, 0, 0, 0, 0, '#000000']; $hex = $color === 'FACE' ? $face : (string)$color; $col = imagecolorallocate($img, hexdec(substr($hex, 1, 2)), hexdec(substr($hex, 3, 2)), hexdec(substr($hex, 5, 2))); match ($kind) { 'rect' => $c > 0 && $d > 0 ? imagefilledrectangle($img, (int)$a, (int)$b, (int)($a + $c - 1), (int)($b + $d - 1), $col) : null, 'line' => imageline($img, (int)$a, (int)$b, (int)$c, (int)$d, $col), 'oval' => imagefilledellipse($img, (int)round($a + $c / 2), (int)round($b + $d / 2), (int)$c, (int)$d, $col), 'ovalOutline' => imageellipse($img, (int)round($a + $c / 2), (int)round($b + $d / 2), (int)$c + 1, (int)$d + 1, $col), 'point' => imagesetpixel($img, (int)$a, (int)$b, $col), default => null, }; } /** @return array<string, mixed> */ private static function data(): array { if (self::$ops === null) { $file = dirname(__DIR__, 2) . '/assets/smileys/ops.json'; self::$ops = is_file($file) ? (array)json_decode((string)file_get_contents($file), true) : []; self::$ops += ['heads' => [], 'eyes' => [], 'mouths' => [], 'hands' => ['left' => [], 'right' => []], 'feature_offset' => ['per_nose' => []], 'big' => []]; } return self::$ops; }}