<?phpnamespace App\Gfx;/** Build-time-free procedural wall posters, shared by native OpenGL and WASM/WebGL. */final class PosterFactory{ public const ATLAS_COLS = 4; public const ATLAS_ROWS = 4; public const TILE_W = 200; public const TILE_H = 290; /** * 4x4 atlas. Four cells keep the banal "VOTRE PUB ICI" gag; the other * twelve are generated by the DeepSeek MinitelRosePoster supplied for the game. */ public static function advert(): \GdImage { $atlas = imagecreatetruecolor(self::ATLAS_COLS * self::TILE_W, self::ATLAS_ROWS * self::TILE_H); imagealphablending($atlas, true); imagesavealpha($atlas, true); imagefill($atlas, 0, 0, imagecolorallocate($atlas, 16, 8, 24)); $rose = new MinitelRosePoster(); for ($i = 0; $i < self::ATLAS_COLS * self::ATLAS_ROWS; $i++) { if ($i % 4 === 0) { $tile = self::advertTile($i); } else { $tile = $rose->generate(361500 + $i * 97); self::zoomPixelatePortrait($tile); } $x = ($i % self::ATLAS_COLS) * self::TILE_W; $y = intdiv($i, self::ATLAS_COLS) * self::TILE_H; imagecopyresized( $atlas, $tile, $x, $y, 0, 0, self::TILE_W, self::TILE_H, imagesx($tile), imagesy($tile), ); imagedestroy($tile); } return $atlas; } /** * Keep DeepSeek's generated portrait, but make it deliberately anonymous: * crop tightly into the face, collapse it to a 10x10 source and blow it back * up with nearest-neighbour blocks. At 320x180 it reads as "Minitel rose", * not as a recognisable person. */ private static function zoomPixelatePortrait(\GdImage $im): void { $tiny = imagecreatetruecolor(10, 10); imagecopyresized($tiny, $im, 0, 0, 100, 90, 10, 10, 200, 200); imagecopyresized($im, $tiny, 50, 45, 0, 0, 300, 300, 10, 10); imagedestroy($tiny); $frame = imagecolorallocate($im, 255, 96, 160); imagerectangle($im, 48, 43, 351, 346, $frame); } private static function advertTile(int $variant): \GdImage { $im = imagecreatetruecolor(MinitelRosePoster::WIDTH, MinitelRosePoster::HEIGHT); imagealphablending($im, true); imagesavealpha($im, true); $ink = imagecolorallocate($im, 18, 10, 28); $paper = imagecolorallocate($im, 245, 214 - ($variant % 3) * 12, 70 + ($variant % 2) * 18); $pink = imagecolorallocate($im, 242, 52, 151); $cyan = imagecolorallocate($im, 55, 226, 226); $white = imagecolorallocate($im, 250, 245, 224); imagefilledrectangle($im, 0, 0, 399, 579, $paper); imagefilledrectangle($im, 14, 14, 385, 565, $ink); imagefilledrectangle($im, 26, 30, 373, 178, $pink); imagefilledrectangle($im, 26, 194, 373, 486, $white); imagefilledrectangle($im, 26, 508, 373, 536, $cyan); // The wall poster is viewed from several metres away in a 320x180 game. // Use deliberately huge bitmap typography so the gag reads immediately. self::drawTextScaled($im, 'VOTRE', 200, 62, $white, 5); self::drawTextScaled($im, 'PUB ICI', 200, 248, $ink, 5); self::drawTextScaled($im, 'EMPLACEMENT', 200, 382, $pink, 2); self::drawTextScaled($im, 'DISPONIBLE', 200, 420, $pink, 2); self::drawTextScaled($im, '3615 / RESERVE', 200, 520, $ink, 1); imagefilledrectangle($im, 38, 210, 72, 244, $cyan); imagefilledrectangle($im, 328, 438, 362, 472, $cyan); imagerectangle($im, 5, 5, 394, 574, $pink); return $im; } private static function drawTextScaled(\GdImage $im, string $text, int $cx, int $y, int $color, int $scale): void { $font = 5; $w = imagefontwidth($font) * strlen($text); $h = imagefontheight($font); $tmp = imagecreatetruecolor(max(1, $w), $h); $white = imagecolorallocate($tmp, 255, 255, 255); $black = imagecolorallocate($tmp, 0, 0, 0); imagefill($tmp, 0, 0, $white); imagestring($tmp, $font, 0, 0, $text, $black); $drawW = $w * $scale; $x0 = $cx - intdiv($drawW, 2); for ($py = 0; $py < $h; $py++) { for ($px = 0; $px < $w; $px++) { if ((imagecolorat($tmp, $px, $py) & 0xFF) < 128) { imagefilledrectangle( $im, $x0 + $px * $scale, $y + $py * $scale, $x0 + ($px + 1) * $scale - 1, $y + ($py + 1) * $scale - 1, $color, ); } } } imagedestroy($tmp); }}