UK
games/satisfying-bounce / src / Lib / Shorts / Sprites.php · commit 8bdd6e4
<?phpnamespace JumPHP\Shorts;use JumPHP\Renderer\Renderer;/** * Pre-rendered shapes for a game played live: a disc, a soft glow or a rectangle of one colour is made once (GD, then a texture) * and then costs ONE Renderer::copy() instead of a fillRect per scan line. With the HD renderer (hd: auto) a ball with its halo and * trail is a thousand fillRect calls a frame, which a phone's browser cannot keep up with; here it is a dozen copies. * * The binding has no colour or alpha modulation, so there is one texture per colour (and alpha): colours are rounded (16 steps of 256 for a disc, 8 for a plain rectangle) and the caches are bounded, so a scene that cycles through hues does not create * textures forever. A film (video:make) does not use this class: it keeps the exact scan-line drawing. */final class Sprites{    private const SIZES = [16, 32, 64, 128];    private const MAX_SOLID = 1200;    private const MAX_DISCS = 700;    /** @var array<int, mixed> rounded colour and alpha => 1 x 1 texture */    private static array $solid = [];    /** @var array<string, mixed> size, kind, alpha, rounded colour => texture */    private static array $discs = [];    /** @var array<string, \GdImage> size|kind|alpha => black shape with an alpha channel */    private static array $shapes = [];    /** textures made since the start (a steady scene makes none: measure it) */    public static int $created = 0;    /** A rectangle $w x $h centred on ($cx, $cy), turned by $angle radians, of a plain colour: one copy. */    public static function rect(Renderer $r, float $cx, float $cy, float $w, float $h, float $angle, array $rgb, int $alpha = 255): void    {        if ($alpha <= 0 || $w <= 0 || $h <= 0) {            return;        }        $r->copy(self::solid($r, $rgb, $alpha), null, $cx - $w / 2, $cy - $h / 2, $w, $h, false, false, $angle * 180 / M_PI);    }    /** A disc / an ellipse (radii $rx along $angle, $ry across) with a soft anti-aliased edge: one copy. */    public static function disc(Renderer $r, float $cx, float $cy, float $rx, float $ry, float $angle, array $rgb, int $alpha = 255): void    {        if ($alpha <= 0 || $rx < 0.15 || $ry < 0.15) {            return;        }        $tex = self::shape($r, 'disc', 2 * max($rx, $ry), $rgb, $alpha, 0);        $r->copy($tex, null, $cx - $rx, $cy - $ry, 2 * $rx, 2 * $ry, false, false, $angle * 180 / M_PI);    }    /** A soft halo of radius $radius * 2.2 (three translucent discs in one picture), $strength 0..2 (1: the usual). */    public static function glow(Renderer $r, float $cx, float $cy, float $radius, array $rgb, float $strength = 1.0): void    {        $level = (int)round(max(0.0, min(2.0, $strength)) * 4);        if ($level <= 0 || $radius < 0.3) {            return;        }        $half = $radius * 2.2;        $tex = self::shape($r, 'glow', 2 * $half, $rgb, 255, $level);        $r->copy($tex, null, $cx - $half, $cy - $half, 2 * $half, 2 * $half);    }    /** Forgets every texture (a new renderer, or to free memory). */    public static function clear(): void    {        foreach (self::$solid as $t) {            \SDL_DestroyTexture($t);        }        foreach (self::$discs as $t) {            \SDL_DestroyTexture($t);        }        self::$solid = self::$discs = self::$shapes = [];    }    /** Destroys the oldest quarter of a cache (a hue that is never seen again leaves; no big flush, no hitch). */    private static function evict(array &$cache): void    {        $drop = intdiv(count($cache), 4);        foreach ($cache as $key => $texture) {            if ($drop-- <= 0) {                break;            }            \SDL_DestroyTexture($texture);            unset($cache[$key]);        }    }    /** @return mixed an SDL texture */    private static function solid(Renderer $r, array $rgb, int $alpha)    {        $a = $alpha >= 247 ? 255 : max(17, (int)(round($alpha / 17) * 17));        $cr = min(255, ((int)$rgb[0] >> 3 << 3) + 4);        $cg = min(255, ((int)$rgb[1] >> 3 << 3) + 4);        $cb = min(255, ((int)$rgb[2] >> 3 << 3) + 4);        $key = ($cr >> 3) << 20 | ($cg >> 3) << 14 | ($cb >> 3) << 8 | ($a >> 4);        if (isset(self::$solid[$key])) {            return self::$solid[$key];        }        if (count(self::$solid) >= self::MAX_SOLID) {            self::evict(self::$solid);        }        self::$created++;        $surface = \SDL_CreateRGBSurface(0, 1, 1, 32, 0x000000FF, 0x0000FF00, 0x00FF0000, -0x1000000);        \SDL_FillRect($surface, new \SDL_Rect(0, 0, 1, 1), \SDL_MapRGBA($surface->format, $cr, $cg, $cb, $a));        $texture = \SDL_CreateTextureFromSurface($r->getRenderer(), $surface);        \SDL_FreeSurface($surface);        return self::$solid[$key] = $texture;    }    /** @return mixed an SDL texture of a coloured shape big enough for $diameter real pixels */    private static function shape(Renderer $r, string $kind, float $diameter, array $rgb, int $alpha, int $level)    {        $real = $diameter * max(1, $r->hd());        $size = self::SIZES[count(self::SIZES) - 1];        foreach (self::SIZES as $s) {            if ($s >= $real * 0.8) {                $size = $s;                break;            }        }        // few alpha levels and 16 steps per colour channel: a hue that drifts (a rainbow, a pulse) reuses the same few hundred textures        $a = $alpha >= 224 ? 255 : ($alpha >= 160 ? 192 : ($alpha >= 112 ? 128 : ($alpha >= 72 ? 96 : ($alpha >= 40 ? 56 : 24))));        $cr = min(255, ((int)$rgb[0] >> 4 << 4) + 8);        $cg = min(255, ((int)$rgb[1] >> 4 << 4) + 8);        $cb = min(255, ((int)$rgb[2] >> 4 << 4) + 8);        $key = "$size|$kind|$a|$level|$cr|$cg|$cb";        if (isset(self::$discs[$key])) {            return self::$discs[$key];        }        if (count(self::$discs) >= self::MAX_DISCS) {            self::evict(self::$discs);        }        self::$created++;        $img = imagecreatetruecolor($size, $size);        imagealphablending($img, false);        imagesavealpha($img, true);        imagecopy($img, self::template($size, $kind, $a, $level), 0, 0, 0, 0, $size, $size);        imagefilter($img, IMG_FILTER_COLORIZE, $cr, $cg, $cb, 0);        $file = sys_get_temp_dir() . '/jumphp-sprite-' . getmypid() . '.png';        imagepng($img, $file, 1);        $surface = \IMG_Load($file);        @unlink($file);        // smooth when scaled (the renderer's default is nearest, for pixel art): the hint is read when the texture is made        \SDL_SetHint(\SDL_HINT_RENDER_SCALE_QUALITY, '1');        $texture = \SDL_CreateTextureFromSurface($r->getRenderer(), $surface);        \SDL_SetHint(\SDL_HINT_RENDER_SCALE_QUALITY, '0');        \SDL_FreeSurface($surface);        return self::$discs[$key] = $texture;    }    /** The black shape with its alpha channel, drawn at 4x and shrunk (anti-aliased), made once per size. */    private static function template(int $size, string $kind, int $alpha, int $level): \GdImage    {        $key = "$size|$kind|$alpha|$level";        if (isset(self::$shapes[$key])) {            return self::$shapes[$key];        }        $big = $size * 4;        $img = imagecreatetruecolor($big, $big);        imagesavealpha($img, true);        imagealphablending($img, false);        imagefill($img, 0, 0, imagecolorallocatealpha($img, 0, 0, 0, 127));        $c = $big / 2;        if ($kind === 'glow') {            // three discs, bigger and fainter outwards (the radii and alphas of Gfx::glow, as parts of the biggest one)            imagealphablending($img, true);            foreach ([[1.0, 20], [0.8473, 28], [0.7055, 38]] as [$k, $a]) {                $gd = 127 - (int)round(min(255, $a * $level / 4) / 255 * 127);                imagefilledellipse($img, (int)$c, (int)$c, (int)($big * $k) - 1, (int)($big * $k) - 1, imagecolorallocatealpha($img, 0, 0, 0, $gd));            }        } else {            $gd = 127 - (int)round($alpha / 255 * 127);            imagefilledellipse($img, (int)$c, (int)$c, $big - 4, $big - 4, imagecolorallocatealpha($img, 0, 0, 0, $gd));        }        $small = imagecreatetruecolor($size, $size);        imagealphablending($small, false);        imagesavealpha($small, true);        imagefill($small, 0, 0, imagecolorallocatealpha($small, 0, 0, 0, 127));        imagecopyresampled($small, $img, 0, 0, 0, 0, $size, $size, $big, $big);        return self::$shapes[$key] = $small;    }}