diff --git a/src/Image.php b/src/Image.php index 3628449..4972ce2 100644 --- a/src/Image.php +++ b/src/Image.php @@ -103,11 +103,10 @@ public function layout(Layout $layout): self public function theme(Theme|BuiltInTheme $theme): self { if ($theme instanceof BuiltInTheme) { - $this->theme = $theme->load(); - } else { - $this->theme = $theme; + $theme = $theme->load(); } + $this->theme = $theme; return $this; } diff --git a/src/Interfaces/Box.php b/src/Interfaces/Box.php new file mode 100644 index 0000000..aa0630e --- /dev/null +++ b/src/Interfaces/Box.php @@ -0,0 +1,14 @@ + + */ + protected array $features = []; - abstract protected function features(string $feature, string $setting): mixed; + public function addFeature(BoxInterface $feature): void + { + $name = $feature->getName() ?? $this->generateFeatureName($feature); + $this->features[$name] = $feature; + } - public function border(Border $border): self + public function getFeature(string $name): ?BoxInterface { - $this->border = $border; - return $this; + return $this->features[$name] ?? null; } - public function callToAction(): string + public function border(Border $border): self { - return $this->config->callToAction; + $this->border = $border; + return $this; } - public function getCallToAction(): TextBox + public function callToAction(): ?string { - return $this->callToAction ??= (new TextBox()) - ->text($this->callToAction()) - ->color($this->config->theme->getCallToActionColor()) - ->font($this->config->theme->getCallToActionFont()) - ->size($this->features('call_to_action', 'font_size')) - ->box(...$this->features('call_to_action', 'dimensions')) - ->position(...$this->features('call_to_action', 'layout')); + return $this->config->callToAction ?? null; } - public function description(): string + public function description(): ?string { - return $this->config->description; + return $this->config->description ?? null; } - public function getDescription(): TextBox + public function picture(): ?string { - return $this->description ??= (new TextBox()) - ->text($this->description()) - ->color($this->config->theme->getDescriptionColor()) - ->font($this->config->theme->getDescriptionFont()) - ->size($this->features('description', 'font_size')) - ->box(...$this->features('description', 'dimensions')) - ->position(...$this->features('description', 'layout')); + return $this->config->picture ?? null; } public function title(): string @@ -68,37 +66,20 @@ public function title(): string return $this->config->title; } - public function getTitle(): TextBox + public function url(): ?string { - return $this->title ??= (new TextBox()) - ->text($this->title()) - ->color($this->config->theme->getTitleColor()) - ->font($this->config->theme->getTitleFont()) - ->size($this->features('title', 'font_size')) - ->box(...$this->features('title', 'dimensions')) - ->position(...$this->features('title', 'layout')); - } + if (!isset($this->config->url)) { + return null; + } - public function url(): string - { return parse_url($this->config->url, PHP_URL_HOST) ?? $this->config->url; } - public function getUrl(): TextBox - { - return $this->url ??= (new TextBox()) - ->text($this->url()) - ->color($this->config->theme->getUrlColor()) - ->font($this->config->theme->getUrlFont()) - ->size($this->features('url', 'font_size')) - ->box(...$this->features('url', 'dimensions')) - ->position(...$this->features('url', 'layout')); - } - /** - * The area within the canvas that we should be rendering content. This is just a convenience object + * The area within the canvas that we should be rendering content. This is just a convenience object to help layout + * of other features and is not normally rendered (it's not added to the $features list) */ - public function mountArea(): Box + public function mountArea(): BoxInterface { return (new Box) ->box( @@ -128,4 +109,9 @@ public function getBorderPosition(): BorderPosition return $this->borderPosition; } + + protected function generateFeatureName(BoxInterface $feature): string + { + return $feature::class . '_' . (count($this->features) + 1); + } } diff --git a/src/Layout/Box.php b/src/Layout/Box.php index e6a7914..d5640b2 100644 --- a/src/Layout/Box.php +++ b/src/Layout/Box.php @@ -4,14 +4,26 @@ use Intervention\Image\Geometry\Point; use Intervention\Image\Geometry\Rectangle; +use Intervention\Image\Interfaces\ImageInterface; +use SimonHamp\TheOg\Interfaces\Box as BoxInterface; -readonly class Box +readonly class Box implements BoxInterface { + public Position $anchor; + public Rectangle $box; - public Position $pivot; + + public string $name; + public Point $position; - public Box $relativeTo; + + /** + * @var Closure + */ + public mixed $relativeTo; + public Position $relativeToPosition; + public Rectangle $renderedBox; public function box(int $width, int $height): self @@ -28,76 +40,88 @@ public function position( int $y, ?callable $relativeTo = null, Position $position = Position::TopLeft, - Position $pivot = Position::TopLeft + Position $anchor = Position::TopLeft ): self { $this->position = new Point($x, $y); if ($relativeTo) { - $this->relativeTo = $relativeTo(); + $this->relativeTo = $relativeTo; $this->relativeToPosition = $position; - $this->pivot = $pivot; } + $this->anchor = $anchor; + return $this; } - + public function calculatePosition(): Point { if (isset($this->relativeTo)) { - $position = $this->relativeTo->getPointForPosition($this->relativeToPosition); + $origin = ($this->relativeTo)(); + + if (! $origin instanceof Point) { + // new Point() + throw new \InvalidArgumentException( + 'The relativeTo callback must return an instance of '.Point::class + ); + } return new Point( - $position->x() + $this->position->x(), - $position->y() + $this->position->y() + $origin->x() + $this->position->x() - $this->anchorOffset()->x(), + $origin->y() + $this->position->y() - $this->anchorOffset()->y() ); } - return $this->position; + return $this->position + ->moveX(-$this->anchorOffset()->x()) + ->moveY(-$this->anchorOffset()->y()); } - public function getPointForPosition(Position $position): Point + /** + * Get the absolute Point on the canvas for a given anchor position on the current box. + */ + public function anchor(?Position $position = null): Point { - $box = $this->getRenderedBox(); + if (! $position) { + $position = $this->anchor; + } + $origin = $this->calculatePosition(); + $anchor = $this->anchorOffset($position); + + return new Point($origin->x() + $anchor->x(), $origin->y() + $anchor->y()); + } + + protected function anchorOffset(?Position $position = null): Point + { + if (! $position) { + $position = $this->anchor; + } + + // We can check pre-rendered boxes here because we know that we don't need the absolute position of the box yet + $box = $this->getPrerenderedBox() ?? $this->getRenderedBox(); + $coordinates = match ($position) { - Position::BottomLeft => [ - $origin->x(), - $origin->y() + $box->height() - ], - Position::BottomRight => [ - $origin->x() + $box->width(), - $origin->y() + $box->height() - ], + Position::BottomLeft => [0, $box->height()], + Position::BottomRight => [$box->width(), $box->height()], Position::Center => [ - $origin->x() + intval(floor($box->width() / 2)), - $origin->y() + intval(floor($box->height() / 2)), + intval(floor($box->width() / 2)), + intval(floor($box->height() / 2)) ], Position::MiddleBottom => [ - $origin->x() + intval(floor($box->width() / 2)), - $origin->y() + $box->height(), - ], - Position::MiddleLeft => [ - $origin->x(), - $origin->y() + intval(floor($box->height() / 2)), + intval(floor($box->width() / 2)), + $box->height() ], + Position::MiddleLeft => [0, intval(floor($box->height() / 2))], Position::MiddleRight => [ - $origin->x() + $box->width(), - $origin->y() + intval(floor($box->height() / 2)), + $box->width(), + intval(floor($box->height() / 2)) ], - Position::MiddleTop => [ - $origin->x() + intval(floor($box->width() / 2)), - $origin->y(), - ], - Position::TopLeft => [ - $origin->x(), - $origin->y() - ], - Position::TopRight => [ - $origin->x() + $box->width(), - $origin->y() - ] + Position::MiddleTop => [intval(floor($box->width() / 2)), 0], + Position::TopLeft => [0, 0], + Position::TopRight => [$box->width(), 0] }; return new Point(...$coordinates); @@ -108,9 +132,43 @@ protected function getRenderedBox(): Rectangle return $this->renderedBox ?? $this->box; } + /** + * Get the box that will be rendered without calculating its position on the canvas. + */ + protected function getPrerenderedBox(): ?Rectangle + { + return null; + } + protected function setRenderedBox(Rectangle $box): self { $this->renderedBox = $box; return $this; } + + public function render(ImageInterface $image): void + { + $position = $this->calculatePosition(); + + $this->box->setBackgroundColor('orange'); + $this->box->setBorder('red'); + $this->box->setPivot($position); + + $image->drawRectangle( + $position->x(), + $position->y(), + $this->box, + ); + } + + public function name(string $name): static + { + $this->name = $name; + return $this; + } + + public function getName(): ?string + { + return $this->name ?? null; + } } diff --git a/src/Layout/Layouts/GitHubBasic.php b/src/Layout/Layouts/GitHubBasic.php index 959ecaa..0dd8739 100644 --- a/src/Layout/Layouts/GitHubBasic.php +++ b/src/Layout/Layouts/GitHubBasic.php @@ -5,58 +5,90 @@ use SimonHamp\TheOg\BorderPosition; use SimonHamp\TheOg\Layout\AbstractLayout; use SimonHamp\TheOg\Layout\Position; +use SimonHamp\TheOg\Layout\TextBox; class GitHubBasic extends AbstractLayout { protected BorderPosition $borderPosition = BorderPosition::Bottom; + protected int $borderWidth = 25; + protected int $height = 640; + protected int $padding = 20; + protected int $width = 1280; - protected function features(string $feature, string $setting): mixed + public function features(): void { - $settings = [ - 'call_to_action' => [ - 'font_size' => 20, - 'dimensions' => [$this->mountArea()->box->width(), 240], - 'layout' => [ - 'x' => 0, - 'y' => 20, - 'relativeTo' => fn () => $this->getDescription(), - ], - ], - 'description' => [ - 'font_size' => 40, - 'dimensions' => [$this->mountArea()->box->width(), 240], - 'layout' => [ - 'x' => 0, - 'y' => 50, - 'relativeTo' => fn () => $this->getTitle(), - 'position' => Position::BottomLeft - ], - ], - 'title' => [ - 'font_size' => 60, - 'dimensions' => [$this->mountArea()->box->width(), 400], - 'layout' => [ - 'x' => 0, - 'y' => 20, - 'relativeTo' => fn () => $this->getUrl(), - 'position' => Position::BottomLeft - ], - ], - 'url' => [ - 'font_size' => 28, - 'dimensions' => [$this->mountArea()->box->width(), 45], - 'layout' => [ - 'x' => 0, - 'y' => 20, - 'relativeTo' => fn () => $this->mountArea(), - ], - ], - ]; - - return $settings[$feature][$setting]; + $this->addFeature((new TextBox()) + ->name('title') + ->text($this->title()) + ->color($this->config->theme->getTitleColor()) + ->font($this->config->theme->getTitleFont()) + ->size(60) + ->box($this->mountArea()->box->width(), 400) + ->position( + x: 0, + y: 0, + relativeTo: function () { + if ($url = $this->getFeature('url')) { + return $url->anchor(Position::BottomLeft) + ->moveY(20); + } + + return $this->mountArea() + ->anchor() + ->moveY(20); + }, + ) + ); + + if ($description = $this->description()) { + $this->addFeature((new TextBox()) + ->name('description') + ->text($description) + ->color($this->config->theme->getDescriptionColor()) + ->font($this->config->theme->getDescriptionFont()) + ->size(40) + ->box($this->mountArea()->box->width(), 240) + ->position( + x: 0, + y: 50, + relativeTo: fn() => $this->getFeature('title')->anchor(Position::BottomLeft), + ) + ); + } + + if ($callToAction = $this->callToAction()) { + $this->addFeature((new TextBox()) + ->text($callToAction) + ->color($this->config->theme->getCallToActionColor()) + ->font($this->config->theme->getCallToActionFont()) + ->size(20) + ->box($this->mountArea()->box->width(), 240) + ->position( + x: 0, + y: 20, + relativeTo: fn() => $this->getFeature('description')->anchor(), + ) + ); + } + + if ($url = $this->url()) { + $this->addFeature((new TextBox()) + ->name('url') + ->text($url) + ->color($this->config->theme->getUrlColor()) + ->font($this->config->theme->getUrlFont()) + ->size(28) + ->box($this->mountArea()->box->width(), 45) + ->position( + x: 0, + y: 20, + relativeTo: fn() => $this->mountArea()->anchor(), + ) + ); + } } } diff --git a/src/Layout/Layouts/Standard.php b/src/Layout/Layouts/Standard.php index caf847d..03d371c 100644 --- a/src/Layout/Layouts/Standard.php +++ b/src/Layout/Layouts/Standard.php @@ -5,6 +5,7 @@ use SimonHamp\TheOg\BorderPosition; use SimonHamp\TheOg\Layout\AbstractLayout; use SimonHamp\TheOg\Layout\Position; +use SimonHamp\TheOg\Layout\TextBox; class Standard extends AbstractLayout { @@ -14,54 +15,89 @@ class Standard extends AbstractLayout protected int $padding = 40; protected int $width = 1200; - public function url(): string + public function features(): void { - return strtoupper(parent::url()); + $this->addFeature((new TextBox()) + ->name('title') + ->text($this->title()) + ->color($this->config->theme->getTitleColor()) + ->font($this->config->theme->getTitleFont()) + ->size(60) + ->box($this->mountArea()->box->width(), 400) + ->position( + x: 0, + y: 0, + relativeTo: function() { + if ($url = $this->getFeature('url')) { + return $url->anchor(Position::BottomLeft) + ->moveY(25); + } + + return $this->mountArea() + ->anchor() + ->moveY(20); + } + ) + ); + + if ($description = $this->description()) { + $this->addFeature((new TextBox()) + ->name('description') + ->text($description) + ->color($this->config->theme->getDescriptionColor()) + ->font($this->config->theme->getDescriptionFont()) + ->size(40) + ->box($this->mountArea()->box->width(), 240) + ->position( + x: 0, + y: 50, + relativeTo: fn() => $this->getFeature('title')->anchor(Position::BottomLeft), + ) + ); + } + + if ($callToAction = $this->callToAction()) { + $this->addFeature((new TextBox()) + ->text($callToAction) + ->color($this->config->theme->getCallToActionColor()) + ->font($this->config->theme->getCallToActionFont()) + ->size(20) + ->box($this->mountArea()->box->width(), 240) + ->position( + x: 0, + y: 20, + relativeTo: function() { + $feature = $this->getFeature('description') ?? $this->getFeature('title'); + return $feature->anchor(); + } + ) + ); + } + + if ($url = $this->url()) { + $this->addFeature((new TextBox()) + ->name('url') + ->text($url) + ->color($this->config->theme->getUrlColor()) + ->font($this->config->theme->getUrlFont()) + ->size(28) + ->box($this->mountArea()->box->width(), 45) + ->position( + x: 0, + y: 20, + relativeTo: fn() => $this->mountArea()->anchor(), + ) + ); + } } - protected function features(string $feature, string $setting): mixed + // XXX: This feels weird... maybe it should happen in the theme? Or let the content decide? + public function url(): string { - $settings = [ - 'call_to_action' => [ - 'font_size' => 20, - 'dimensions' => [$this->mountArea()->box->width(), 240], - 'layout' => [ - 'x' => 0, - 'y' => 20, - 'relativeTo' => fn () => $this->getDescription(), - ], - ], - 'description' => [ - 'font_size' => 40, - 'dimensions' => [$this->mountArea()->box->width(), 240], - 'layout' => [ - 'x' => 0, - 'y' => 50, - 'relativeTo' => fn () => $this->getTitle(), - 'position' => Position::BottomLeft - ], - ], - 'title' => [ - 'font_size' => 60, - 'dimensions' => [$this->mountArea()->box->width(), 400], - 'layout' => [ - 'x' => 0, - 'y' => 20, - 'relativeTo' => fn () => $this->getUrl(), - 'position' => Position::BottomLeft - ], - ], - 'url' => [ - 'font_size' => 28, - 'dimensions' => [$this->mountArea()->box->width(), 45], - 'layout' => [ - 'x' => 0, - 'y' => 20, - 'relativeTo' => fn () => $this->mountArea(), - ], - ], - ]; + if ($url = parent::url()) { + return strtoupper($url); + } - return $settings[$feature][$setting]; + return ''; } } diff --git a/src/Layout/Layouts/TwoUp.php b/src/Layout/Layouts/TwoUp.php new file mode 100644 index 0000000..ffd0e68 --- /dev/null +++ b/src/Layout/Layouts/TwoUp.php @@ -0,0 +1,93 @@ +picture()) { + $this->addFeature((new PictureBox()) + ->path($picture) + ->box($this->width / 2, $this->height) + ->position( + x: 0, + y: 0, + ) + ); + } + + $this->addFeature((new TextBox()) + ->text($this->title()) + ->color($this->config->theme->getTitleColor()) + ->font($this->config->theme->getTitleFont()) + ->size(56) + ->box($this->mountArea()->box->width() / 2, 400) + ->position( + x: 0, + y: 0, + relativeTo: function () { + if ($url = $this->getFeature('url')) { + return $url->anchor(Position::BottomLeft) + ->moveY(40); + } + + return $this->mountArea() + ->anchor(Position::MiddleTop) + ->moveX(40) + ->moveY(20); + }, + ) + ); + + if ($callToAction = $this->callToAction()) { + $this->addFeature((new TextBox()) + ->name('call_to_action') + ->text($callToAction) + ->color($this->config->theme->getCallToActionColor()) + ->font($this->config->theme->getCallToActionFont()) + ->size(36) + ->box($this->mountArea()->box->width() / 2, 100) + ->position( + x: 0, + y: 0, + relativeTo: fn() => $this->mountArea()->anchor(Position::BottomRight), + anchor: Position::BottomRight, + ) + ); + } + + if ($url = $this->url()) { + $this->addFeature((new TextBox()) + ->name('url') + ->text($url) + ->color($this->config->theme->getUrlColor()) + ->font($this->config->theme->getUrlFont()) + ->size(28) + ->box($this->mountArea()->box->width(), 45) + ->position( + x: 40, + y: 20, + relativeTo: fn() => $this->mountArea()->anchor(Position::MiddleTop), + ) + ); + } + } + + public function url(): string + { + return strtoupper(parent::url()); + } +} diff --git a/src/Layout/PictureBox.php b/src/Layout/PictureBox.php new file mode 100644 index 0000000..6a30c2b --- /dev/null +++ b/src/Layout/PictureBox.php @@ -0,0 +1,28 @@ +read(file_get_contents($this->path)) + ->cover($this->box->width(), $this->box->height()); + + $image->place($picture); + } + + public function path(string $path): self + { + $this->path = $path; + return $this; + } +} diff --git a/src/Layout/TextBox.php b/src/Layout/TextBox.php index ffbab17..7a673ce 100644 --- a/src/Layout/TextBox.php +++ b/src/Layout/TextBox.php @@ -2,9 +2,12 @@ namespace SimonHamp\TheOg\Layout; +use Intervention\Image\Geometry\Point; +use Intervention\Image\Geometry\Polygon; use Intervention\Image\Geometry\Rectangle; use Intervention\Image\Colors\Rgb\Color; use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver; +use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Modifiers\TextModifier; use Intervention\Image\Typography\FontFactory; use Intervention\Image\Typography\TextBlock; @@ -63,17 +66,24 @@ public function vAlign(string $vAlign): self return $this; } - public function render(): CustomTextModifier + public function render(ImageInterface $image): void { - return $this->ensureTextFitsBox($this->generateModifier($this->text)); + $this->ensureTextFitsBox($this->generateModifier($this->text, $this->calculatePosition()))->apply($image); } - protected function generateModifier(string $text): CustomTextModifier + protected function getPrerenderedBox(): Rectangle + { + $modifier = $this->generateModifier($this->text); + + return $this->getFinalTextBox($modifier); + } + + protected function generateModifier(string $text, Point $position = new Point()): CustomTextModifier { return new CustomTextModifier( new TextModifier( $text, - $this->calculatePosition(), + $position, (new FontFactory( function(FontFactory $factory) { $factory->filename($this->font->path()); @@ -99,7 +109,7 @@ protected function doesTextFitInBox(Rectangle $renderedBox): bool return $renderedBox->fitsInto($this->box); } - protected function getRenderedBoxForText(string $text, CustomTextModifier $modifier): Rectangle + protected function getRenderedBoxForText(string $text, CustomTextModifier $modifier): Rectangle|Polygon { return $modifier->boundingBox($this->getTextBlock($text)); } @@ -110,13 +120,20 @@ protected function getTextBlock(string $text): TextBlock } protected function ensureTextFitsBox(CustomTextModifier $modifier): CustomTextModifier + { + $this->getFinalTextBox($modifier); + + return $modifier; + } + + protected function getFinalTextBox(CustomTextModifier &$modifier): Rectangle { $text = $this->text; $renderedBox = $this->getRenderedBoxForText($text, $modifier); while (! $this->doesTextFitInBox($renderedBox)) { if ($renderedBox->width() > $this->box->width()) { - $text = wordwrap($this->text, intval(floor($this->box->width() / ($modifier->boxSize('M')->width() / 1.8)))); + $text = wordwrap($text, intval(floor($this->box->width() / ($modifier->boxSize('M')->width() / 1.8)))); $renderedBox = $this->getRenderedBoxForText($text, $modifier); } @@ -132,11 +149,9 @@ protected function ensureTextFitsBox(CustomTextModifier $modifier): CustomTextMo $renderedBox = $this->getRenderedBoxForText($text, $modifier); } - $modifier = $this->generateModifier($text); + $modifier = $this->generateModifier($text, $modifier->position); } - $this->setRenderedBox($renderedBox); - - return $modifier; + return $renderedBox; } } diff --git a/src/Theme/Theme.php b/src/Theme/Theme.php index c06d36f..1a1a677 100644 --- a/src/Theme/Theme.php +++ b/src/Theme/Theme.php @@ -28,8 +28,7 @@ protected function lightTheme(): ThemeInterface backgroundColor: '#ECEBE4', baseColor: '#153B50', baseFont: Inter::bold(), - callToActionBackgroundColor: '#153B50', - callToActionColor: '#ECEBE4', + callToActionColor: '#153B50', descriptionColor: '#429EA6', descriptionFont: Inter::light(), titleFont: Inter::black(), @@ -49,7 +48,6 @@ protected function darkTheme(): ThemeInterface descriptionColor: '#3F4045', descriptionFont: Inter::light(), titleFont: Inter::black(), - urlColor: '#30292F', ) extends AbstractTheme {}; } } diff --git a/src/Traits/RendersFeatures.php b/src/Traits/RendersFeatures.php index 6105e83..37c68ba 100644 --- a/src/Traits/RendersFeatures.php +++ b/src/Traits/RendersFeatures.php @@ -3,14 +3,12 @@ namespace SimonHamp\TheOg\Traits; use Imagick; -use Intervention\Image\Geometry\Point; use Intervention\Image\Image; use Intervention\Image\ImageManager; use SimonHamp\TheOg\Border; use SimonHamp\TheOg\BorderPosition; use SimonHamp\TheOg\Image as Config; use SimonHamp\TheOg\Interfaces\Background; -use SimonHamp\TheOg\Layout\TextBox; use SimonHamp\TheOg\Theme\BackgroundPlacement; trait RendersFeatures @@ -28,41 +26,17 @@ public function render(Config $config): Image $this->canvas = $this->manager->create($this->width, $this->height) ->fill($this->config->theme->getBackgroundColor()); - // TODO: This would be better as a homogenous stack where we can simply add items of a given type (Box) to the - // stack. Render could simply loop over the items on the stack and call `render` on each one - - // Worth noting here that the order of the items in the stack should determine the order of execution, which - // may have implications on the rendering of later elements due to dependencies on rendered dimensions - - // Basically, it would be up to the layout developer to know the order of the dependencies and layering needs - // of the design and reconcile that themselves by ordering the stack appropriately within the layout class - if ($this->config->theme->getBackground() instanceof Background) { $this->renderBackground(); } - if (isset($this->config->backgroundUrl) && $backgroundUrl = $this->getUrl($this->config->backgroundUrl)) { - - - $this->renderBackgroundUrl(); - } - - if (isset($this->config->url) && $url = $this->getUrl($this->config->url)) { - $this->renderTextBox($url); - } - - if (isset($this->config->title) && $title = $this->getTitle($this->config->title)) { - $this->renderTextBox($title); - } - - if (isset($this->config->description) && $description = $this->getDescription($this->config->description)) { - $this->renderTextBox($description); - } - - // TODO: Render callToActionBackground + // Loop over the stack of features and render each to the canvas + // The order of the items in the stack will determine the order in which they are rendered and thus their + // 'layering' on the canvas: earlier elements will be rendered 'underneath' later elements. + $this->features(); - if (isset($this->config->callToAction) && $callToAction = $this->getCallToAction($this->config->callToAction)) { - $this->renderTextBox($callToAction); + foreach ($this->features as $feature) { + $feature->render($this->canvas); } if (! isset($this->border)) { @@ -79,11 +53,6 @@ public function render(Config $config): Image return $this->canvas; } - protected function renderTextBox(TextBox $textBox): void - { - $textBox->render()->apply($this->canvas); - } - protected function renderBorder(): void { match ($this->border->getPosition()) { diff --git a/tests/Integration/ImageTest.php b/tests/Integration/ImageTest.php index a793800..d401659 100644 --- a/tests/Integration/ImageTest.php +++ b/tests/Integration/ImageTest.php @@ -6,6 +6,8 @@ use SimonHamp\TheOg\Background as BuiltInBackground; use SimonHamp\TheOg\BorderPosition; use SimonHamp\TheOg\Image; +use SimonHamp\TheOg\Layout\Layouts\GitHubBasic; +use SimonHamp\TheOg\Layout\Layouts\TwoUp; use SimonHamp\TheOg\Theme\Background; use SimonHamp\TheOg\Theme\BackgroundPlacement; use SimonHamp\TheOg\Theme\Theme; @@ -25,20 +27,25 @@ public function test_basic_image(Image $image, string $name): void $this->assertMatchesImageSnapshot($path); } - #[DataProvider('snapshotImages')] - public function test_stringify_image(Image $image, string $name): void - { - $this->assertIsString($image->toString()); - } - public static function snapshotImages(): iterable { + yield 'test layout' => [ + (new Image())->layout(new TestLayout()), + 'test-layout', + ]; + yield 'basic' => [ + (new Image()) + ->title('Just a standard og:image for a blog post with a simple title'), + 'basic', + ]; + + yield 'more text features' => [ (new Image()) ->url('https://example.com/blog/some-blog-post-url') ->title('Some blog post title that is quite big and quite long') ->description('Some slightly smaller but potentially much longer subtext. It could be really long so we might need to trim it completely after many words'), - 'basic', + 'more-text-features', ]; yield 'different theme' => [ @@ -92,5 +99,48 @@ public static function snapshotImages(): iterable ->background(new Background('https://placehold.co/600x400.png', 0.2)), 'different-theme-with-background-url', ]; + + yield 'github layout' => [ + (new Image()) + ->layout(new GitHubBasic) + ->url('username/repo') + ->title('An awesome package') + ->background(BuiltInBackground::CloudyDay, 0.8), + 'githubbasic-layout', + ]; + + yield 'twoup layout' => [ + (new Image()) + ->layout(new TwoUp) + ->accentColor('#cc0000') + /** + * Photo by Matthew Hamilton on Unsplash + * @see https://unsplash.com/@thatsmrbio?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash + * @see https://unsplash.com/photos/unpaired-red-adidas-sneaker-pO2bglTMJpo?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash + */ + ->picture(__DIR__.'/../resources/product.jpg') + ->url('https://my-ecommerce-store.com/') + ->title('This layout is great for eCommerce!') + ->callToAction('Buy Now →') + ->background(BuiltInBackground::CloudyDay, 0.8), + 'twoup-layout', + ]; + + yield 'twoup dark' => [ + (new Image()) + ->layout(new TwoUp) + ->theme(Theme::Dark) + ->accentColor('#c33') + /** + * Photo by Matthew Hamilton on Unsplash + * @see https://unsplash.com/@thatsmrbio?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash + * @see https://unsplash.com/photos/unpaired-red-adidas-sneaker-pO2bglTMJpo?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash + */ + ->picture(__DIR__.'/../resources/product.jpg') + ->url('https://my-ecommerce-store.com/') + ->title('This layout is great for eCommerce!') + ->callToAction('ONLY $99!'), + 'twoup-dark', + ]; } } diff --git a/tests/Integration/TestLayout.php b/tests/Integration/TestLayout.php new file mode 100644 index 0000000..6504873 --- /dev/null +++ b/tests/Integration/TestLayout.php @@ -0,0 +1,35 @@ +addFeature((new Box) + ->box(100, 100) + ->position( + x: 0, + y: 0, + relativeTo: fn () => $this->mountArea()->anchor(Position::BottomLeft), + anchor: Position::Center + ) + ); + } +} diff --git a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set background stretched to cover__1.png b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set background stretched to cover__1.png index fda3395..33ab05d 100644 Binary files a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set background stretched to cover__1.png and b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set background stretched to cover__1.png differ diff --git a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set basic with background url__1.png b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set basic with background url__1.png index 3f43fe2..deb3d03 100644 Binary files a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set basic with background url__1.png and b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set basic with background url__1.png differ diff --git a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set basic__1.png b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set basic__1.png index d3fe65d..1855958 100644 Binary files a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set basic__1.png and b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set basic__1.png differ diff --git a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set different theme with background url__1.png b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set different theme with background url__1.png index 4f4026b..a8c2b49 100644 Binary files a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set different theme with background url__1.png and b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set different theme with background url__1.png differ diff --git a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set different theme__1.png b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set different theme__1.png index 433ff9b..8c89513 100644 Binary files a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set different theme__1.png and b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set different theme__1.png differ diff --git a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set github layout__1.png b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set github layout__1.png new file mode 100644 index 0000000..ed3add4 Binary files /dev/null and b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set github layout__1.png differ diff --git a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set more text features__1.png b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set more text features__1.png new file mode 100644 index 0000000..70f775f Binary files /dev/null and b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set more text features__1.png differ diff --git a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set override some elements__1.png b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set override some elements__1.png index af861da..ee3749b 100644 Binary files a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set override some elements__1.png and b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set override some elements__1.png differ diff --git a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set test layout__1.png b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set test layout__1.png new file mode 100644 index 0000000..21c4e3a Binary files /dev/null and b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set test layout__1.png differ diff --git a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set twoup dark__1.png b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set twoup dark__1.png new file mode 100644 index 0000000..3f0c9b0 Binary files /dev/null and b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set twoup dark__1.png differ diff --git a/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set twoup layout__1.png b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set twoup layout__1.png new file mode 100644 index 0000000..a51d273 Binary files /dev/null and b/tests/Integration/__snapshots__/ImageTest__test_basic_image with data set twoup layout__1.png differ diff --git a/tests/resources/product.jpg b/tests/resources/product.jpg new file mode 100644 index 0000000..2d41cbf Binary files /dev/null and b/tests/resources/product.jpg differ