*/ public array $rows = []; /** @var array{0: int, 1: int, 2: int, 3: int}|null x1, y1, x2, y2 changed */ private ?array $dirty = null; public function __construct(public int $width = 0, public int $height = 0) { $this->resize($width, $height); } public function resize(int $width, int $height): void { $this->width = max(0, $width); $this->height = max(0, $height); $this->rows = array_fill(0, $this->height, str_repeat("\0\0\0\0", $this->width)); $this->dirty = $this->width > 0 && $this->height > 0 ? [0, 0, $this->width, $this->height] : null; } /** Raw pixels of a rectangle (w * h * 4 bytes, rows from the top). */ public function put(int $x, int $y, int $w, int $h, string $pixels): void { $stride = $w * 4; // the rows of $pixels: the rectangle's own width $skip = max(0, -$x); $x = max(0, $x); $w = min($w - $skip, $this->width - $x); for ($row = 0; $row < $h; $row++) { $line = $y + $row; if ($line < 0 || $line >= $this->height || $w <= 0) { continue; } $this->rows[$line] = substr_replace($this->rows[$line], substr($pixels, $row * $stride + $skip * 4, $w * 4), $x * 4, $w * 4); } $this->mark($x, $y, $w, $h); } /** CopyRect: a rectangle moved from (sx, sy) (a window dragged, a terminal scrolled). */ public function copy(int $sx, int $sy, int $x, int $y, int $w, int $h): void { $pieces = []; for ($row = 0; $row < $h; $row++) { $pieces[] = substr($this->rows[$sy + $row] ?? '', $sx * 4, $w * 4); } $this->put($x, $y, $w, $h, implode('', array_map(fn ($p) => str_pad($p, $w * 4, "\0"), $pieces))); } public function fill(int $x, int $y, int $w, int $h, string $pixel): void { $this->put($x, $y, $w, $h, str_repeat($pixel, max(0, $w * $h))); } public function mark(int $x, int $y, int $w, int $h): void { $x2 = min($this->width, $x + $w); $y2 = min($this->height, $y + $h); $x = max(0, $x); $y = max(0, $y); if ($x2 <= $x || $y2 <= $y) { return; } $this->dirty = $this->dirty === null ? [$x, $y, $x2, $y2] : [min($this->dirty[0], $x), min($this->dirty[1], $y), max($this->dirty[2], $x2), max($this->dirty[3], $y2)]; } /** @return array{0: int, 1: int, 2: int, 3: int}|null the changed rectangle (x, y, w, h), forgotten once asked */ public function takeDirty(): ?array { $d = $this->dirty; $this->dirty = null; return $d === null ? null : [$d[0], $d[1], $d[2] - $d[0], $d[3] - $d[1]]; } /** A rectangle as a 32-bit BMP file (top-down): SDL_LoadBMP_RW reads it without a copy of each pixel in PHP. */ public function bmp(int $x, int $y, int $w, int $h): string { $data = ''; for ($row = $y; $row < $y + $h; $row++) { $data .= substr($this->rows[$row], $x * 4, $w * 4); } return self::bmpHeader($w, $h, strlen($data)) . $data; } public static function bmpHeader(int $w, int $h, int $size): string { // the height negative = rows from the top; written as an unsigned 32-bit value (the same bytes on 32 and 64-bit PHP) return 'BM' . pack('VvvV', 54 + $size, 0, 0, 54) . pack('VV', 40, $w) . pack('v', (-$h) & 0xffff) . pack('v', 0xffff) . pack('vvVVVVVV', 1, 32, 0, $size, 2835, 2835, 0, 0); } /** The colour of a pixel (tests): [r, g, b]. */ public function pixel(int $x, int $y): array { $p = substr($this->rows[$y] ?? '', $x * 4, 4); return strlen($p) === 4 ? [ord($p[2]), ord($p[1]), ord($p[0])] : [0, 0, 0]; } }