본문 바로가기
css

주 이미지 색상 간 전환을 사용하여 프레임 애니메이션화

by code-box 2022. 2. 22.
반응형

즉시 고지 사항 - 이미지 프레임에 애니메이션을 적용하는 것이 아니라 이미지 뒤에 있는 배경에 애니메이션을 적용하고 패딩을 추가하는 것입니다.

저는 제 프로젝트 중 하나에 업로드된 이미지 그리드에 있는 몇몇 이미지에 더 많은 관심을 끌고 싶었습니다. 무난한 카드 디자인을 하거나 이미지 주변의 테두리와 그림자를 수정하지 않고 이 기술을 고안해냈습니다.

이 아이디어는 메인 이미지 색상을 추출하고 그 사이의 전환을 통해 그라데이션(gradient)을 구성하는 것입니다. 이 그라데이션은 이미지를 "원 둘레"로 만들어 더욱 생동감 있고 활기차게 보이게 합니다.
동시에, 각 이미지는 고유한 애니메이션 그라데이션이 있을 것이며, 이는 이미지, 이미지 프레임, 용기 배경색 간의 전환을 더욱 원활하게 만들어 줄 것이라고 생각합니다.

이를 위해 이미지마다 메인 이미지 색상을 추출한다. 이 색의 "팔레트"는 앞서 언급한 그라데이션의 구성에 사용된다.

 

몇 가지 예:

Palette 1Palette 2Palette 3Palette 4

 

이 코드는 케플러 겔로트의 영향을 많이 받았다.
이미지 색 추출

입력 매개 변수는 이미지의 경로, 추출할 색상 수 및 델타 - 색상 값을 정량화할 때의 간격(1-255)입니다. 델타가 작을수록 색상이 정확하지만 유사한 색상의 수도 증가합니다.

<?php

declare(strict_types=1);

namespace App\Service;

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로 추가됩니다.

최종 결과(미리보기 목적으로 크기가 감소):

 

Grid layout

Twig 확장(필터)을 만들어 Symfony 프로젝트에 포함시켰습니다.

<?php

declare(strict_types=1);

namespace App\Twig;

use App\Service\ColorPaletteExtractor;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

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.dig.tig

<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에서 다운로드한 테스트용 사진

댓글