<?phpnamespace Tools\Art;use App\Engine\Palette;/** A palette picture drawn in 2D (textures, flats, the status bar): pixels wrap around (tileable). */final class Canvas{ /** @var int[] */ public array $px; private const FONT = [ 'A' => ['.#.', '#.#', '###', '#.#', '#.#'], 'B' => ['##.', '#.#', '##.', '#.#', '##.'], 'C' => ['.##', '#..', '#..', '#..', '.##'], 'D' => ['##.', '#.#', '#.#', '#.#', '##.'], 'E' => ['###', '#..', '##.', '#..', '###'], 'F' => ['###', '#..', '##.', '#..', '#..'], 'G' => ['.##', '#..', '#.#', '#.#', '.##'], 'H' => ['#.#', '#.#', '###', '#.#', '#.#'], 'I' => ['###', '.#.', '.#.', '.#.', '###'], 'K' => ['#.#', '#.#', '##.', '#.#', '#.#'], 'L' => ['#..', '#..', '#..', '#..', '###'], 'M' => ['#.#', '###', '###', '#.#', '#.#'], 'N' => ['##.', '#.#', '#.#', '#.#', '#.#'], 'O' => ['.#.', '#.#', '#.#', '#.#', '.#.'], 'P' => ['##.', '#.#', '##.', '#..', '#..'], 'R' => ['##.', '#.#', '##.', '#.#', '#.#'], 'S' => ['.##', '#..', '.#.', '..#', '##.'], 'T' => ['###', '.#.', '.#.', '.#.', '.#.'], 'U' => ['#.#', '#.#', '#.#', '#.#', '###'], 'V' => ['#.#', '#.#', '#.#', '#.#', '.#.'], 'W' => ['#.#', '#.#', '###', '###', '#.#'], 'X' => ['#.#', '#.#', '.#.', '#.#', '#.#'], 'Y' => ['#.#', '#.#', '.#.', '.#.', '.#.'], 'Z' => ['###', '..#', '.#.', '#..', '###'], '0' => ['###', '#.#', '#.#', '#.#', '###'], '1' => ['.#.', '##.', '.#.', '.#.', '###'], '2' => ['##.', '..#', '.#.', '#..', '###'], '3' => ['##.', '..#', '.#.', '..#', '##.'], '4' => ['#.#', '#.#', '###', '..#', '..#'], '5' => ['###', '#..', '##.', '..#', '##.'], '6' => ['.##', '#..', '###', '#.#', '###'], '7' => ['###', '..#', '.#.', '.#.', '.#.'], '8' => ['###', '#.#', '###', '#.#', '###'], '9' => ['###', '#.#', '###', '..#', '##.'], '-' => ['...', '...', '###', '...', '...'], ' ' => ['...', '...', '...', '...', '...'], '%' => ['#.#', '..#', '.#.', '#..', '#.#'], ]; public function __construct(public int $w, public int $h, int $fill = Palette::TRANSPARENT) { $this->px = array_fill(0, $w * $h, $fill); } public function set(int $x, int $y, int $c): void { $x = (($x % $this->w) + $this->w) % $this->w; $y = (($y % $this->h) + $this->h) % $this->h; $this->px[$y * $this->w + $x] = $c; } public function get(int $x, int $y): int { $x = (($x % $this->w) + $this->w) % $this->w; $y = (($y % $this->h) + $this->h) % $this->h; return $this->px[$y * $this->w + $x]; } public function rect(int $x, int $y, int $w, int $h, int $c): void { for ($j = 0; $j < $h; $j++) { for ($i = 0; $i < $w; $i++) { $this->set($x + $i, $y + $j, $c); } } } /** A function of (x, y) => palette index over a rectangle (the whole canvas by default). */ public function paint(callable $f, int $x = 0, int $y = 0, ?int $w = null, ?int $h = null): void { $w ??= $this->w; $h ??= $this->h; for ($j = 0; $j < $h; $j++) { for ($i = 0; $i < $w; $i++) { $c = $f($x + $i, $y + $j, $i, $j); if ($c !== null) { $this->set($x + $i, $y + $j, $c); } } } } /** A raised plate: lit top / left edges, dark bottom / right, noisy face. */ public function plate(int $x, int $y, int $w, int $h, int $ramp, int $shade, int $edge = 1, float $noise = 1.5, bool $sunken = false): void { $this->paint(function (int $px, int $py, int $i, int $j) use ($w, $h, $ramp, $shade, $edge, $noise, $sunken): int { $s = $shade + (int)round((Noise::hash($px, $py, 7) - 0.5) * $noise); $hi = $sunken ? -2 : 2; if ($i < $edge || $j < $edge) { $s += $hi; } elseif ($i >= $w - $edge || $j >= $h - $edge) { $s -= $hi; } return Palette::index($ramp, $s); }, $x, $y, $w, $h); } public function rivet(int $x, int $y, int $ramp, int $shade): void { $this->set($x, $y, Palette::index($ramp, $shade + 3)); $this->set($x + 1, $y, Palette::index($ramp, $shade + 1)); $this->set($x, $y + 1, Palette::index($ramp, $shade + 1)); $this->set($x + 1, $y + 1, Palette::index($ramp, $shade - 3)); } public function line(int $x0, int $y0, int $x1, int $y1, int $c): void { $n = max(abs($x1 - $x0), abs($y1 - $y0), 1); for ($i = 0; $i <= $n; $i++) { $this->set((int)round($x0 + ($x1 - $x0) * $i / $n), (int)round($y0 + ($y1 - $y0) * $i / $n), $c); } } /** Text in a 3x5 font, $scale pixels per dot. */ public function text(int $x, int $y, string $text, int $c, int $scale = 1, ?int $shadow = null): void { foreach (str_split(strtoupper($text)) as $k => $ch) { $glyph = self::FONT[$ch] ?? self::FONT[' ']; foreach ($glyph as $gy => $row) { for ($gx = 0; $gx < 3; $gx++) { if ($row[$gx] === '#') { if ($shadow !== null) { $this->rect($x + ($k * 4 + $gx) * $scale + 1, $y + $gy * $scale + 1, $scale, $scale, $shadow); } $this->rect($x + ($k * 4 + $gx) * $scale, $y + $gy * $scale, $scale, $scale, $c); } } } } } public function blit(array $sprite, int $x, int $y): void { for ($j = 0; $j < $sprite['h']; $j++) { for ($i = 0; $i < $sprite['w']; $i++) { $c = ord($sprite['px'][$j * $sprite['w'] + $i]); if ($c !== Palette::TRANSPARENT) { $this->set($x + $i, $y + $j, $c); } } } } public function rowMajor(): string { return implode('', array_map('chr', $this->px)); } public function colMajor(): string { $s = ''; for ($x = 0; $x < $this->w; $x++) { for ($y = 0; $y < $this->h; $y++) { $s .= chr($this->px[$y * $this->w + $x]); } } return $s; }}