SONIC PHP

by davidnurdin · Platform
not rated yet
▶ Play Source Edit GitHub
. / src / Gfx / CrtFilter.php · commit d114180
<?phpnamespace App\Gfx;/** * Options > FILTRE CRT: an old TV look drawn over the scaled picture, in one or two copies per frame: *  - scanlines: the lower part of every line of the game picture is darkened; *  - vignette: the corners get darker, smoothly. * Both are RGBA surfaces turned into textures (a surface with an alpha channel gives a blended texture: * the SDL extension has no SDL_SetTextureBlendMode / SDL_SetRenderDrawBlendMode), made again only when * the size of the picture changes. */class CrtFilter{    private const SCANLINE_ALPHA = 105;   // darkness of a scanline (0-255)    private const VIGNETTE_ALPHA = 150;   // darkness of the corners    private const VIGNETTE_SIZE = 48;     // the vignette is computed small, then smoothly stretched    private mixed $scanlines = null;    private mixed $vignette = null;    private string $key = '';    public function __construct(private View $renderer)    {    }    /** Draws the filter over $dst, the picture of $viewHeight game lines scaled on the window. */    public function draw(\SDL_Rect $dst, int $viewHeight): void    {        if ($dst->w <= 0 || $dst->h <= 0 || $viewHeight <= 0) return;        $key = $dst->h . 'x' . $viewHeight;        if ($key !== $this->key) {            $this->free();            $this->scanlines = $this->makeScanlines($dst->h, $viewHeight);            $this->vignette ??= $this->makeVignette();            $this->key = $key;        }        $sdlRenderer = $this->renderer->getRenderer();        if ($this->scanlines) \SDL_RenderCopy($sdlRenderer, $this->scanlines, null, $dst);        if ($this->vignette) \SDL_RenderCopy($sdlRenderer, $this->vignette, null, $dst);    }    public function free(): void    {        if ($this->scanlines) \SDL_DestroyTexture($this->scanlines);        $this->scanlines = null;        $this->key = '';    }    /** 1 pixel wide, as high as the picture on screen: stretched across it. */    private function makeScanlines(int $height, int $lines): mixed    {        $surface = $this->surface(1, $height);        if (!$surface) return null;        $scale = $height / $lines;        $rect = new \SDL_Rect;        for ($i = 0; $i < $lines; $i++) {            $top = (int)floor($i * $scale);            $bottom = (int)floor(($i + 1) * $scale);            $rows = $bottom - $top;            if ($rows <= 0) continue;            if ($rows === 1) {                // a game line on one screen line (small window): every other line, lighter                if ($i % 2) $this->fill($surface, $rect, 0, $top, 1, 1, (int)(self::SCANLINE_ALPHA * 0.6));                continue;            }            // the lower ~40% of the line is dark, the first screen line of the dark part a bit less            $dark = max(1, (int)round($rows * 0.4));            $this->fill($surface, $rect, 0, $bottom - $dark, 1, $dark, self::SCANLINE_ALPHA);            if ($dark >= 3) $this->fill($surface, $rect, 0, $bottom - $dark, 1, 1, (int)(self::SCANLINE_ALPHA * 0.6));        }        return $this->texture($surface, false);    }    /** Darker towards the corners (squared distance from the centre, on an ellipse). */    private function makeVignette(): mixed    {        $n = self::VIGNETTE_SIZE;        $surface = $this->surface($n, $n);        if (!$surface) return null;        $rect = new \SDL_Rect;        for ($y = 0; $y < $n; $y++) {            for ($x = 0; $x < $n; $x++) {                $dx = ($x + 0.5) / $n * 2 - 1;                $dy = ($y + 0.5) / $n * 2 - 1;                $d = sqrt($dx * $dx * 0.7 + $dy * $dy * 0.7);                $alpha = (int)round(self::VIGNETTE_ALPHA * max(0, min(1, ($d - 0.55) / 0.6)) ** 2);                if ($alpha > 0) $this->fill($surface, $rect, $x, $y, 1, 1, $alpha);            }        }        return $this->texture($surface, true);    }    /** Transparent RGBA surface. The alpha mask is written -0x1000000 (0xFF000000): it must fit a 32-bit PHP int (wasm). */    private function surface(int $w, int $h): mixed    {        return \SDL_CreateRGBSurface(0, $w, $h, 32, 0x000000FF, 0x0000FF00, 0x00FF0000, -0x1000000) ?: null;    }    private function fill($surface, \SDL_Rect $rect, int $x, int $y, int $w, int $h, int $alpha): void    {        [$rect->x, $rect->y, $rect->w, $rect->h] = [$x, $y, $w, $h];        \SDL_FillRect($surface, $rect, \SDL_MapRGBA($surface->format, 0, 0, 0, $alpha));    }    private function texture($surface, bool $smooth): mixed    {        // the scale quality is read when the texture is created; the game keeps "0" (crisp pixels)        \SDL_SetHint(\SDL_HINT_RENDER_SCALE_QUALITY, $smooth ? "1" : "0");        $texture = \SDL_CreateTextureFromSurface($this->renderer->getRenderer(), $surface);        \SDL_SetHint(\SDL_HINT_RENDER_SCALE_QUALITY, "0");        \SDL_FreeSurface($surface);        return $texture ?: null;    }}