Sign in
EN
apps/caramail-chat / src / Gui / SmileyPainter.php · commit 6eebcdb
<?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;    }}