Сразу оговорюсь — речь идет не об анимации рамок изображения, а об анимации фона за изображением и добавлении некоторой подложки.
В одном из своих проектов я хотел привлечь больше внимания к некоторым изображениям в сетке загруженных изображений. Вместо того чтобы использовать простой дизайн открытки или настраивать границы и тени вокруг этих изображений, я придумал эту технику.
Идея заключается в том, чтобы выделить основные цвета изображения и создать градиент с переходами между ними. Этот градиент будет «обходить» изображение, делая его более живым и ярким.
В то же время каждое изображение будет иметь свой уникальный анимированный градиент, который, на мой взгляд, сделает переход между изображением, рамкой изображения и цветом фона контейнера более плавным.
Для этого для каждого изображения выделяются основные цвета изображения. Эта «палитра» цветов используется при составлении вышеупомянутого градиента.
Несколько примеров:
На код в основном повлиял этот скрипт Кеплера Гелотта:
Image Color Extract
Входными параметрами являются путь к изображению, количество цветов для извлечения и дельта — величина разрыва при квантовании значений цвета (1-255). Чем меньше дельта, тем точнее цвет, но также увеличивается количество похожих цветов.
<?php
declare(strict_types=1);
namespace AppService;
class ColorPaletteExtractor
{
private const PREVIEW_WIDTH = 250;
private const PREVIEW_HEIGHT = 250;
public function extractMainImgColors(string $imagePath, int $colorsCount, int $delta): array
{
$halfDelta = 0;
if ($delta > 2) {
$halfDelta = $delta / 2 - 1;
}
$size = getimagesize($imagePath);
$scale = 1;
if ($size[0] > 0) {
$scale = min(self::PREVIEW_WIDTH / $size[0], self::PREVIEW_HEIGHT / $size[1]);
}
$width = (int) $size[0];
$height = (int) $size[1];
if ($scale < 1) {
$width = (int) floor($scale * $size[0]);
$height = (int) floor($scale * $size[1]);
}
$imageResized = imagecreatetruecolor($width, $height);
$imageType = $size[2];
$imageOriginal = null;
if (IMG_JPEG === $imageType) {
$imageOriginal = imagecreatefromjpeg($imagePath);
}
if (IMG_GIF === $imageType) {
$imageOriginal = imagecreatefromgif($imagePath);
}
if (IMG_PNG === $imageType) {
$imageOriginal = imagecreatefrompng($imagePath);
}
imagecopyresampled($imageResized, $imageOriginal, 0, 0, 0, 0, $width, $height, $size[0], $size[1]);
$img = $imageResized;
$imgWidth = imagesx($img);
$imgHeight = imagesy($img);
$totalPixelCount = 0;
$hexArray = [];
for ($y = 0; $y < $imgHeight; $y++) {
for ($x = 0; $x < $imgWidth; $x++) {
$totalPixelCount++;
$index = imagecolorat($img, $x, $y);
$colors = imagecolorsforindex($img, $index);
if ($delta > 1) {
$colors['red'] = intval((($colors['red']) + $halfDelta) / $delta) * $delta;
$colors['green'] = intval((($colors['green']) + $halfDelta) / $delta) * $delta;
$colors['blue'] = intval((($colors['blue']) + $halfDelta) / $delta) * $delta;
if ($colors['red'] >= 256) {
$colors['red'] = 255;
}
if ($colors['green'] >= 256) {
$colors['green'] = 255;
}
if ($colors['blue'] >= 256) {
$colors['blue'] = 255;
}
}
$hex = substr('0' . dechex($colors['red']), -2)
. substr('0' . dechex($colors['green']), -2)
. substr('0' . dechex($colors['blue']), -2);
if (!isset($hexArray[$hex])) {
$hexArray[$hex] = 0;
}
$hexArray[$hex]++;
}
}
// Reduce gradient colors
arsort($hexArray, SORT_NUMERIC);
$gradients = [];
foreach ($hexArray as $hex => $num) {
if (!isset($gradients[$hex])) {
$newHexValue = $this->findAdjacent((string) $hex, $gradients, $delta);
$gradients[$hex] = $newHexValue;
} else {
$newHexValue = $gradients[$hex];
}
if ($hex != $newHexValue) {
$hexArray[$hex] = 0;
$hexArray[$newHexValue] += $num;
}
}
// Reduce brightness variations
arsort($hexArray, SORT_NUMERIC);
$brightness = [];
foreach ($hexArray as $hex => $num) {
if (!isset($brightness[$hex])) {
$newHexValue = $this->normalize((string) $hex, $brightness, $delta);
$brightness[$hex] = $newHexValue;
} else {
$newHexValue = $brightness[$hex];
}
if ($hex != $newHexValue) {
$hexArray[$hex] = 0;
$hexArray[$newHexValue] += $num;
}
}
arsort($hexArray, SORT_NUMERIC);
// convert counts to percentages
foreach ($hexArray as $key => $value) {
$hexArray[$key] = (float) $value / $totalPixelCount;
}
if ($colorsCount > 0) {
return array_slice($hexArray, 0, $colorsCount, true);
} else {
return $hexArray;
}
}
private function normalize(string $hex, array $hexArray, int $delta): string
{
$lowest = 255;
$highest = 0;
$colors['red'] = hexdec(substr($hex, 0, 2));
$colors['green'] = hexdec(substr($hex, 2, 2));
$colors['blue'] = hexdec(substr($hex, 4, 2));
if ($colors['red'] < $lowest) {
$lowest = $colors['red'];
}
if ($colors['green'] < $lowest) {
$lowest = $colors['green'];
}
if ($colors['blue'] < $lowest) {
$lowest = $colors['blue'];
}
if ($colors['red'] > $highest) {
$highest = $colors['red'];
}
if ($colors['green'] > $highest) {
$highest = $colors['green'];
}
if ($colors['blue'] > $highest) {
$highest = $colors['blue'];
}
// Do not normalize white, black, or shades of grey unless low delta
if ($lowest == $highest) {
if ($delta > 32) {
return $hex;
}
if ($lowest == 0 || $highest >= (255 - $delta)) {
return $hex;
}
}
for (; $highest < 256; $lowest += $delta, $highest += $delta) {
$newHexValue = substr('0' . dechex($colors['red'] - $lowest), -2)
. substr('0' . dechex($colors['green'] - $lowest), -2)
. substr('0' . dechex($colors['blue'] - $lowest), -2);
if (isset($hexArray[$newHexValue])) {
// same color, different brightness - use it instead
return $newHexValue;
}
}
return $hex;
}
private function findAdjacent(string $hex, array $gradients, int $delta)
{
$red = hexdec(substr($hex, 0, 2));
$green = hexdec(substr($hex, 2, 2));
$blue = hexdec(substr($hex, 4, 2));
if ($red > $delta) {
$newHexValue = substr('0' . dechex($red - $delta), -2)
. substr('0' . dechex($green), -2)
. substr('0' . dechex($blue), -2);
if (isset($gradients[$newHexValue])) {
return $gradients[$newHexValue];
}
}
if ($green > $delta) {
$newHexValue = substr('0' . dechex($red), -2)
. substr('0' . dechex($green - $delta), -2)
. substr('0' . dechex($blue), -2);
if (isset($gradients[$newHexValue])) {
return $gradients[$newHexValue];
}
}
if ($blue > $delta) {
$newHexValue = substr('0' . dechex($red), -2)
. substr('0' . dechex($green), -2)
. substr('0' . dechex($blue - $delta), -2);
if (isset($gradients[$newHexValue])) {
return $gradients[$newHexValue];
}
}
if ($red < (255 - $delta)) {
$newHexValue = substr('0' . dechex($red + $delta), -2)
. substr('0' . dechex($green), -2)
. substr('0' . dechex($blue), -2);
if (isset($gradients[$newHexValue])) {
return $gradients[$newHexValue];
}
}
if ($green < (255 - $delta)) {
$newHexValue = substr('0' . dechex($red), -2)
. substr('0' . dechex($green + $delta), -2)
. substr('0' . dechex($blue), -2);
if (isset($gradients[$newHexValue])) {
return $gradients[$newHexValue];
}
}
if ($blue < (255 - $delta)) {
$newHexValue = substr('0' . dechex($red), -2)
. substr('0' . dechex($green), -2)
. substr('0' . dechex($blue + $delta), -2);
if (isset($gradients[$newHexValue])) {
return $gradients[$newHexValue];
}
}
return $hex;
}
}
Я получал наилучшие результаты при работе с 6-10 цветами и дельтой между 20 и 35.
CSS довольно прост:
.gradient-background-animation {
background-size: 400% 400% !important;
animation: BackgroundGradient 10s ease infinite;
}
.card-picture-container {
padding: 0.875rem; // This value determines how big the "frame" will be
}
.card-picture-container img {
object-fit: contain;
max-width: 100%;
}
@keyframes BackgroundGradient {
0% {
background-position: 0 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
Вы можете настроить макет и остальные стили по своему вкусу.
HTML:
<div class="container">
<div class="card-picture-container gradient-background-animation"
style="background-image: linear-gradient(-45deg, #78785a,#5a783c,#3c5a1e,#969678,#b4b4b4,#1e1e1e,#d2d2f0)">
<picture>
<img src="/images/source/image.jpeg" alt="">
</picture>
</div>
</div>
Как вы можете заметить, фон добавлен как встроенный CSS.
Конечный результат (размер уменьшен для целей предварительного просмотра):
Я создал расширение Twig (фильтр) и включил его в свой проект Symfony.
<?php
declare(strict_types=1);
namespace AppTwig;
use AppServiceColorPaletteExtractor;
use TwigExtensionAbstractExtension;
use TwigTwigFilter;
class GradientExtractorExtension extends AbstractExtension
{
public function __construct(
private ColorPaletteExtractor $gradientExtractor,
) {
}
public function getFilters(): array
{
return [
new TwigFilter('get_gradient', [$this, 'getGradient']),
];
}
public function getGradient(string $imagePath, int $colorsCount, int $delta): string
{
$colors = $this->gradientExtractor->extractMainImgColors($imagePath, $colorsCount, $delta);
$filteredColors = array_filter($colors, fn (float $percentage) => $percentage > 0);
return join(',', array_map(fn (string $color) => '#' . $color, array_keys($filteredColors)));
}
}
image-grid.html.twig
<div class="container">
{% for image in images %}
<div class="card-picture-container gradient-background-animation"
style="background-image: linear-gradient(-45deg, {{ asset(image)|get_gradient(7,30) }}); animation-duration: 8s;">
<picture>
<img src="{{ asset(image) }}" alt="">
</picture>
</div>
{% endfor %}
</div>
Фотографии для тестирования загружены с сайта https://www.parkovihrvatske.hr/parkovi