*/ protected array $sources = []; /** * Frame delays of animation frames in seconds. * * @var array */ protected array $delays = []; /** * Frame processing call names. * * @var array */ protected array $processingCalls = []; /** * Frame processing arguments of calls. * * @var array> */ protected array $processingArguments = []; /** * Create new instance. */ public function __construct( protected int $width, protected int $height, null|callable $animation = null, ) { if (is_callable($animation)) { $animation($this); } } /** * {@inheritdoc} * * @see AnimationFactoryInterface::build() */ public static function build( int $width, int $height, callable $animation, DriverInterface $driver, ): ImageInterface { return (new self($width, $height, $animation))->image($driver); } /** * {@inheritdoc} * * @see AnimationFactoryInterface::add() */ public function add(mixed $source, float $delay = 1): AnimationFactoryInterface { $this->currentFrameNumber++; $this->sources[$this->currentFrameNumber] = $source; $this->delays[$this->currentFrameNumber] = $delay; $this->processingCalls[$this->currentFrameNumber] = null; $this->processingArguments[$this->currentFrameNumber] = null; return $this; } /** * {@inheritdoc} * * @see AnimationFactoryInterface::image() */ public function image(DriverInterface $driver): ImageInterface { if (count($this->sources) === 0) { return $driver->createImage($this->width, $this->height); } $frames = array_map( $this->buildFrame(...), array_fill(0, count($this->sources), $driver), $this->sources, $this->delays, $this->processingCalls, $this->processingArguments, ); return new Image($driver, $driver->createCore($frames)); } /** * Build frame from given image source and delay. * * @param null|array $processingArguments */ private function buildFrame( DriverInterface $driver, mixed $source, float $delay, ?string $processingCall = null, ?array $processingArguments = null, ): FrameInterface { try { // try to decode image source $image = $driver->decodeImage($source); } catch (DecoderException | FilesystemException) { // create empty image with colored background $image = $driver->createImage($this->width, $this->height) ->fill($driver->decodeColor($source)); } // adjust size if necessary if ($image->width() !== $this->width || $image->height() !== $this->height) { $image->cover($this->width, $this->height); } // apply processing call if available if ($processingCall !== null) { call_user_func_array([$image, $processingCall], $processingArguments); } // return ready-made frame with all attributes return $image ->core() ->first() ->setDelay($delay) ->setDisposalMethod(DisposalMethod::BACKGROUND->value); } /** * Collect processing calls on frame images. * * @param array> $arguments * @throws Error */ public function __call(string $name, array $arguments): self { if (!method_exists(Image::class, $name)) { throw new Error('Call to undefined method ' . Image::class . '::' . $name . '()'); } $this->processingCalls[$this->currentFrameNumber] = $name; $this->processingArguments[$this->currentFrameNumber] = $arguments; return $this; } }