'Relative to browser', 'fixed' => 'Fixed to browser', 'local' => 'Local to browser', ]; private const POSITION_OPTS = [ 'top left' => 'Top left', 'top center' => 'Top center', 'top right' => 'Top right', 'center left' => 'Center left', 'center center' => 'Center', 'center right' => 'Center right', 'bottom left' => 'Bottom left', 'bottom center' => 'Bottom center', 'bottom right' => 'Bottom right', ]; private const REPEAT_OPTS = [ 'repeat' => 'Repeat', 'repeat-x' => 'Repeat horizontally', 'repeat-y' => 'Repeat vertically', 'space' => 'Repeat without clipping', 'round' => 'Repeat with stretching', 'no-repeat' => 'Don\'t repeat', ]; private const SIZE_OPTS = [ 'auto auto' => 'Determine automatically', 'cover' => 'Cover entire screen', 'contain' => 'Contain within screen', ]; private const SLIDE_DIRS = [ 'tl' => 'Top left', 'tc' => 'Top', 'tr' => 'Top right', 'cl' => 'Left', 'cr' => 'Right', 'bl' => 'Bottom left', 'bc' => 'Bottom', 'br' => 'Bottom right', ]; private const SLIDE_MIN = 0.01; private const SLIDE_MAX = 1000; private const SPIN_DIRS = [ 'cw' => 'Clockwise', 'ccw' => 'Counterclockwise', ]; private const SPIN_MIN = 0.01; private const SPIN_MAX = 1000; private $imageUpload = null; private $slide = false; private $slideSpeed = 1; private $slideDirection = 'br'; private $spin = false; private $spinSpeed = 1; private $spinDirection = 'cw'; private $attachment = null; private $colour = null; private $position = null; private $repeat = null; private $size = null; private $syncWithAudio = false; private $gradient = null; private $gradientAnimate = false; public function getEffectName(): string { return 'Background Image'; } public function getEffectProperties(): array { return [ [ 'name' => 'img', 'title' => 'Image', 'type' => [ 'name' => 'upload', 'allowed' => self::IMG_MIME, ], ], [ 'name' => 'sld', 'title' => 'Slide Animation', 'default' => false, 'type' => [ 'name' => 'bool', ], ], [ 'name' => 'sldspd', 'title' => 'Slide Animation Period', 'default' => 1, 'type' => [ 'name' => 'float', 'min' => self::SLIDE_MIN, 'max' => self::SLIDE_MAX, ], ], [ 'name' => 'slddir', 'title' => 'Slide Animation Direction', 'default' => 'br', 'type' => [ 'name' => 'select', 'options' => self::SLIDE_DIRS, ], ], [ 'name' => 'spin', 'title' => 'Spin Animation', 'default' => false, 'type' => [ 'name' => 'bool', ], ], [ 'name' => 'spnspd', 'title' => 'Spin Animation Period', 'default' => 1, 'type' => [ 'name' => 'float', 'min' => self::SPIN_MIN, 'max' => self::SPIN_MAX, ], ], [ 'name' => 'spndir', 'title' => 'Spin Animation Direction', 'default' => 'br', 'type' => [ 'name' => 'select', 'options' => self::SPIN_DIRS, ], ], [ 'name' => 'attach', 'title' => 'Attachment', 'type' => [ 'name' => 'select', 'options' => self::ATTACH_OPTS, ], ], [ 'name' => 'col', 'title' => 'Colour', 'type' => [ 'name' => 'colour', ], ], [ 'name' => 'pos', 'title' => 'Position', 'type' => [ 'name' => 'select', 'options' => self::POSITION_OPTS, ], ], [ 'name' => 'rep', 'title' => 'Repeat', 'type' => [ 'name' => 'select', 'options' => self::REPEAT_OPTS, ], ], [ 'name' => 'size', 'title' => 'Size', 'type' => [ 'name' => 'select', 'options' => self::SIZE_OPTS, ], ], [ 'name' => 'sync', 'title' => 'Synchronise with Background Audio', 'default' => false, 'type' => [ 'name' => 'bool', ], ], [ 'name' => 'grad', 'title' => 'Gradient', 'default' => [], 'type' => [ 'name' => 'gradient', ], ], /*[ 'name' => 'grdanm', 'title' => 'Apply animations to gradient', 'default' => false, 'type' => [ 'name' => 'bool', ], ],*/ ]; } public function setEffectParams(array $vars, bool $quiet = false): void { try { if(isset($vars['img']) && is_string($vars['img'])) { try { $this->imageUpload = Upload::byId($vars['img']); } catch(Exception $ex) { if(!$quiet) throw $ex; } if(!$quiet && !in_array($this->imageUpload->getType(), self::IMG_MIME)) throw new PageEffectException('Image upload was of invalid type.'); } } catch(UploadNotFoundException $ex) { $this->imageUpload = null; } if(isset($vars['sld'])) $this->slide = is_bool($vars['sld']) ? $vars['sld'] : (is_string($vars['sld']) ? boolval($vars['sld']) : false); if(isset($vars['spin'])) $this->spin = is_bool($vars['spin']) ? $vars['spin'] : (is_string($vars['spin']) ? boolval($vars['spin']) : false); if(isset($vars['sync'])) $this->syncWithAudio = is_bool($vars['sync']) ? $vars['sync'] : (is_string($vars['sync']) ? boolval($vars['sync']) : false); if(isset($vars['grdanm'])) $this->gradientAnimate = is_bool($vars['grdanm']) ? $vars['grdanm'] : (is_string($vars['grdanm']) ? boolval($vars['grdanm']) : false); if(isset($vars['sldspd'])) { $this->slideSpeed = is_int($vars['sldspd']) || is_float($vars['sldspd']) ? $vars['sldspd'] : (is_string($vars['sldspd']) ? floatval($vars['sldspd']) : 1); if(!$quiet && ($this->slideSpeed < self::SLIDE_MIN || $this->slideSpeed > self::SLIDE_MAX)) throw new PageEffectException(sprintf('Slide speed may not be less than %d or more than %d', self::SLIDE_MIN, self::SLIDE_MAX)); } if(isset($vars['spnspd'])) { $this->spinSpeed = is_int($vars['spnspd']) || is_float($vars['spnspd']) ? $vars['spnspd'] : (is_string($vars['spnspd']) ? floatval($vars['spnspd']) : 1); if(!$quiet && ($this->spinSpeed < self::SPIN_MIN || $this->spinSpeed > self::SPIN_MAX)) throw new PageEffectException(sprintf('Spin speed may not be less than %d or more than %d', self::SPIN_MIN, self::SPIN_MAX)); } if(isset($vars['slddir']) && is_string($vars['slddir']) && array_key_exists($vars['slddir'], self::SLIDE_DIRS)) $this->slideDirection = $vars['slddir']; if(isset($vars['spndir']) && is_string($vars['spndir']) && array_key_exists($vars['spndir'], self::SPIN_DIRS)) $this->spinDirection = $vars['spndir']; if(isset($vars['col'])) $this->colour = Colour::create($vars['col']); try { if(isset($vars['grad'])) $this->gradient = Gradient::fromArray($vars['grad']); } catch(Exception $ex) { if(!$quiet) throw $ex; } if(isset($vars['attach']) && is_string($vars['attach']) && array_key_exists($vars['attach'], self::ATTACH_OPTS)) $this->attachment = $vars['attach']; if(isset($vars['pos']) && is_string($vars['pos']) && array_key_exists($vars['pos'], self::POSITION_OPTS)) $this->position = $vars['pos']; if(isset($vars['rep']) && is_string($vars['rep']) && array_key_exists($vars['rep'], self::REPEAT_OPTS)) $this->repeat = $vars['rep']; if(isset($vars['size']) && is_string($vars['size']) && array_key_exists($vars['size'], self::SIZE_OPTS)) $this->size = $vars['size']; } public function getEffectParams(): array { return [ 'img' => empty($this->imageUpload) ? null : $this->imageUpload->getId(), 'sld' => $this->slide, 'sldspd' => $this->slideSpeed, 'slddir' => $this->slideDirection, 'spin' => $this->spin, 'spnspd' => $this->spinSpeed, 'spndir' => $this->spinDirection, 'attach' => $this->attachment, 'col' => empty($this->colour) ? 0 : $this->colour->getRaw(), 'pos' => $this->position, 'rep' => $this->repeat, 'size' => $this->size, 'sync' => $this->syncWithAudio, 'grad' => empty($this->gradient) ? null : $this->gradient->getArray(), 'grdanm' => $this->gradientAnimate, ]; } public function applyEffect(PageBuilder $builder): void { $head = $builder->getHead(); $body = $builder->getContainer(); $bgTarget = $body; $styleText = ''; if(empty($_GET['preview']) && !$this->gradientAnimate) { $bgTargetClasses = ['BackgroundImage']; if($this->spin && $this->slide) $bgTargetClasses[] = 'Cover'; $bgTarget = $body->appendChild(new HtmlTag('div', ['class' => implode(' ', $bgTargetClasses), 'id' => 'BackgroundImage'])); } if(empty($_GET['preview']) && !empty($this->imageUpload)) { if($this->slide) { $imageSize = getimagesize($this->imageUpload->getPath()); $imageWidth = $imageSize[0]; $imageHeight = $imageSize[1]; $animSX = $this->slideDirection[1] === 'l' ? $imageWidth : 0; $animSY = $this->slideDirection[0] === 't' ? $imageHeight : 0; $animEX = $this->slideDirection[1] === 'r' ? $imageWidth : 0; $animEY = $this->slideDirection[0] === 'b' ? $imageHeight : 0; if($imageSize !== false) { $slideAnim = '_' . bin2hex(random_bytes(8)); $styleText .= sprintf( '@keyframes %s { 0%% { background-position: %dpx %dpx; } 100%% { background-position: %dpx %dpx; } } ', $slideAnim, $animSX, $animSY, $animEX, $animEY ); } } } $styleBgColour = isset($this->colour) ? sprintf(' background-color: #%s;', $this->colour->getHex()) : ' background-color: #000;'; $animation = []; $backgroundImage = []; if($this->spin) $animation[] = sprintf('SharedAnimation_Spin360 infinite linear %s %Fs', $this->spinDirection === 'cw' ? 'normal' : 'reverse', $this->spinSpeed); if(isset($slideAnim)) $animation[] = sprintf('%s infinite linear %Fs', $slideAnim, $this->slideSpeed); $syncWithAudio = empty($_GET['preview']) && $this->syncWithAudio; if(!empty($this->imageUpload) && !$syncWithAudio) $backgroundImage[] = sprintf('url(\'%s\')', $this->imageUpload->getUrl()); if($bgTarget !== $body) { $styleText .= '#container {'; if(!empty($this->gradient)) $styleText .= 'background-image: ' . $this->gradient->getCSS() . ';'; $styleText .= $styleBgColour; $styleText .= '}'; } $styleText .= '#' . ($bgTarget->getAttribute('id')) . ' {'; if($bgTarget === $body) { if(!empty($this->gradient)) $backgroundImage[] = $this->gradient->getCSS(); $styleText .= $styleBgColour; } if(!empty($backgroundImage)) $styleText .= sprintf(' background-image: %s;', implode(', ', $backgroundImage)); if(!empty($this->attachment)) $styleText .= sprintf(' background-attachment: %s;', $this->attachment); if(!empty($this->position)) $styleText .= sprintf(' background-position: %s;', $this->position); if(!empty($this->repeat)) $styleText .= sprintf(' background-repeat: %s;', $this->repeat); if(!empty($this->size)) $styleText .= sprintf(' background-size: %s;', $this->size); if(!empty($animation)) $styleText .= sprintf(' animation: %s;', implode(', ', $animation)); $styleText .= ' }'; $styleTag = new HtmlTag('style', ['type' => 'text/css']); $styleTag->setTextContent($styleText); $head->appendChild($styleTag); if(!empty($this->imageUpload) && $syncWithAudio) { $scriptText = 'window.addEventListener(\'DOMContentLoaded\', function() {'; $scriptText .= 'synchroniseBackgroundWithAudio(\'' . $this->imageUpload->getUrl() . '\');'; $scriptText .= '});'; $scriptTag = new HtmlTag('script', ['type' => 'text/javascript']); $scriptTag->setTextContent($scriptText); $head->appendChild($scriptTag); } } }