rounded colour and alpha => 1 x 1 texture */ private static array $solid = []; /** @var array size, kind, alpha, rounded colour => texture */ private static array $discs = []; /** @var array 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; } }