Bitmap, 'delay' => ミリ秒], ...])。静止画は空 */ public array $frames = []; /** アニメーションの繰り返し回数(0 = 無限) */ public int $loops = 0; public function __construct(int $width, int $height, ?string $data = null) { self::check($width, $height); $this->width = $width; $this->height = $height; $this->data = $data ?? str_repeat("\0\0\0\0", $width * $height); if (strlen($this->data) !== $width * $height * 4) throw new \InvalidArgumentException('画素データの大きさが合いません。'); } /** * 画像の大きさを確認する(読み込む前に、壊れた・大き過ぎるファイルで落ちない様に) */ public static function check(int $width, int $height): void { if ($width < 1 || $height < 1) throw new \RuntimeException("画像の大きさが不正です({$width}x{$height})。"); if ($width * $height > self::MAX_PIXELS) throw new \RuntimeException("画像が大き過ぎます({$width}x{$height})。"); } /** 1色で塗った画像 */ public static function fill(int $width, int $height, int $r, int $g, int $b, int $a = 255): self { self::check($width, $height); return new self($width, $height, str_repeat(chr($r).chr($g).chr($b).chr($a), $width * $height)); } /** * 画素を読む * * @return array [r, g, b, a] */ public function get(int $x, int $y): array { $o = ($y * $this->width + $x) * 4; return [ord($this->data[$o]), ord($this->data[$o + 1]), ord($this->data[$o + 2]), ord($this->data[$o + 3])]; } public function set(int $x, int $y, int $r, int $g, int $b, int $a = 255): void { $o = ($y * $this->width + $x) * 4; $this->data[$o] = chr($r); $this->data[$o + 1] = chr($g); $this->data[$o + 2] = chr($b); $this->data[$o + 3] = chr($a); } /** 1行分の画素(RGBA) */ public function row(int $y): string { return substr($this->data, $y * $this->width * 4, $this->width * 4); } /** 全ての画素が不透明か */ public function isOpaque(): bool { $len = strlen($this->data); for ($i = 3; $i < $len; $i += 4) if ($this->data[$i] !== "\xff") return false; return true; } /** 全ての画素が灰色(R = G = B)か */ public function isGray(): bool { $len = strlen($this->data); for ($i = 0; $i < $len; $i += 4) { if ($this->data[$i] !== $this->data[$i + 1] || $this->data[$i] !== $this->data[$i + 2]) return false; } return true; } /** * 別の画像を重ねる(はみ出した部分は切る) * * @param bool $blend true:アルファで合成(over)、false:そのまま置き換える(source) */ public function draw(Bitmap $src, int $dx, int $dy, bool $blend = true): void { $x0 = max(0, $dx); $x1 = min($this->width, $dx + $src->width); if ($x0 >= $x1) return; for ($sy = max(0, -$dy); $sy < $src->height && $sy + $dy < $this->height; $sy++) { $line = substr($src->data, ($sy * $src->width + $x0 - $dx) * 4, ($x1 - $x0) * 4); $at = (($sy + $dy) * $this->width + $x0) * 4; if ($blend) $line = self::over(substr($this->data, $at, strlen($line)), $line); $this->data = substr_replace($this->data, $line, $at, strlen($line)); } } /** 範囲を1色にする */ public function clear(int $x, int $y, int $w, int $h, int $r = 0, int $g = 0, int $b = 0, int $a = 0): void { $this->draw(self::fill(max(1, $w), max(1, $h), $r, $g, $b, $a), $x, $y, false); } /** * RGBAの列のアルファ合成($fgを$bgの上に) */ public static function over(string $bg, string $fg): string { // 殆どの画素は完全に不透明か透明なので、その場合は計算しない $len = strlen($fg); $out = $bg; for ($i = 0; $i < $len; $i += 4) { $fa = ord($fg[$i + 3]); if ($fa === 255) { $out[$i] = $fg[$i]; $out[$i + 1] = $fg[$i + 1]; $out[$i + 2] = $fg[$i + 2]; $out[$i + 3] = "\xff"; } else if ($fa !== 0) { $ba = ord($bg[$i + 3]); $oa = $fa + intdiv($ba * (255 - $fa) + 127, 255); for ($c = 0; $c < 3; $c++) { $v = (ord($fg[$i + $c]) * $fa * 255 + ord($bg[$i + $c]) * $ba * (255 - $fa)) / (255 * $oa); $out[$i + $c] = chr((int)round($v)); } $out[$i + 3] = chr($oa); } } return $out; } public function copy(): self { $b = new self($this->width, $this->height, $this->data); $b->frames = $this->frames; $b->loops = $this->loops; return $b; } }