Анимируйте кадр с помощью перехода между основными цветами изображения

Сразу оговорюсь — речь идет не об анимации рамок изображения, а об анимации фона за изображением и добавлении некоторой подложки.

В одном из своих проектов я хотел привлечь больше внимания к некоторым изображениям в сетке загруженных изображений. Вместо того чтобы использовать простой дизайн открытки или настраивать границы и тени вокруг этих изображений, я придумал эту технику.

Идея заключается в том, чтобы выделить основные цвета изображения и создать градиент с переходами между ними. Этот градиент будет «обходить» изображение, делая его более живым и ярким.
В то же время каждое изображение будет иметь свой уникальный анимированный градиент, который, на мой взгляд, сделает переход между изображением, рамкой изображения и цветом фона контейнера более плавным.

Для этого для каждого изображения выделяются основные цвета изображения. Эта «палитра» цветов используется при составлении вышеупомянутого градиента.

Несколько примеров:

На код в основном повлиял этот скрипт Кеплера Гелотта:
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

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *