Зображення, яке ще не завантажилося, залишає на сторінці порожнечу. Зазвичай це вирішують за допомогою сірих прямокутників, розмитих мініатюр (blurhash) або просто залишають пусте місце. Простіший, але ефективний спосіб — заповнити простір середнім кольором фото. Так макет виглядатиме завершеним ще до того, як з'явиться перший піксель справжнього зображення.
Laravel 13.24 додав метод dominantColor() до свого native image API. Це дозволяє отримати потрібний колір без встановлення додаткових пакетів. У цій статті ми розберемося, як дізнатися цей колір, де його зберігати та як використовувати.
# Налаштування
Драйвери GD та Imagick для роботи із зображеннями використовують Intervention Image. Це рекомендована залежність, яку потрібно встановити вручну:
composer require intervention/image:^4.0
Більше нічого не потрібно — визначення домінантного кольору працює на обох драйверах.
# Отримання домінантного кольору
Викличте dominantColor() на будь-якому екземплярі зображення, щоб отримати hex-рядок:
use Illuminate\Support\Facades\Image;
$color = Image::fromPath(storage_path('app/photo.jpg'))->dominantColor();
// "#8a6f4c"
Метод працює так: він створює копію зображення, стискає її до одного пікселя та зчитує його значення. Це саме середній колір, а не той, що зустрічається найчастіше. Наприклад, фото червоної комори на зеленому полі дасть брудно-оливковий колір, а не червоний. Для заповнювача (placeholder) це ідеально, адже колір має слугувати фоном для всього зображення, а не виділяти окремий об'єкт.
Результат завжди містить префікс решітки (#). Зображення з alpha channel повертають восьмизначний код (#rrggbbaa) — врахуйте це при виборі розміру колонки в базі даних.
Трансформації застосовуються до того, як знімається колірна проба. Тобто ви отримуєте колір того зображення, яке збираєтеся зберегти, а не оригіналу:
$color = Image::fromStorage('uploads/photo.jpg')
->crop(800, 600, x: 200, y: 0)
->dominantColor();
Відріжте небо — і колір зміниться.
# Зберігання кольору разом із завантаженням
Колір корисний лише тоді, коли він доступний під час рендерингу сторінки. Тому найкраще розрахувати його один раз при завантаженні та зберегти в модель. Почнемо з міграції:
Schema::create('photos', function (Blueprint $table) {
$table->id();
$table->string('path');
$table->string('dominant_color', 9)->nullable();
$table->timestamps();
});
У контролері створіть зображення та використовуйте цей екземпляр повторно:
use App\Models\Photo;
public function store(Request $request)
{
$request->validate([
'photo' => ['required', 'image', 'max:12288'],
]);
$photo = $request->image('photo')
->orient()
->scale(width: 1600)
->optimize();
return Photo::create([
'path' => $photo->store('photos'),
'dominant_color' => $photo->dominantColor(),
]);
}
Тут важливий порядок: store() виконує всі операції, кешує оброблені байти в екземплярі та очищує конвеєр (pipeline). Наступний виклик dominantColor() працює вже з цими закешованими даними без зайвих зусиль. Якщо поміняти рядки місцями, колір буде розраховано на тимчасовому клоні, а store() знову оброблятиме зображення. Результат той самий, але роботи вдвічі більше.
Значення також мемоїзується всередині екземпляра, тому повторні виклики dominantColor() безкоштовні. Оскільки кожна трансформація повертає новий екземпляр, мініатюра, створена з того ж джерела, розрахує свій власний колір:
$source = Image::fromStorage('uploads/photo.jpg')->orient();
$thumbnail = $source->cover(300, 300);
$display = $source->scale(width: 1600);
$thumbnail->dominantColor(); // розраховано для кропнутого квадрата
$display->dominantColor(); // розраховано для повного кадру
# Використання кольору як placeholder
Коли колір збережений у моделі, шаблон Blade перетворюється на блок із заданим фоном та правильним співвідношенням сторін (aspect ratio):
<div class="aspect-[3/2] overflow-hidden rounded-lg"
style="background-color: {{ $photo->dominant_color }}">
<img src="{{ Storage::url($photo->path) }}"
alt="{{ $photo->caption }}"
loading="lazy"
class="h-full w-full object-cover">
</div>
Кольоровий блок з’являється миттєво, а зображення плавно завантажується поверх нього. Це ідеально поєднується з loading="lazy" у великих галереях, де картинки підтягуються лише під час скролу.
# Читабельність тексту поверх кольору
Підписи або іконки поверх заповнювача мають залишатися читабельними незалежно від того, що на фото: снігове поле чи нічне небо. Розрахунок відносної яскравості (relative luminance) допоможе визначити поріг у кілька рядків:
namespace App\Support;
class Color
{
public static function isLight(string $hex): bool
{
[$r, $g, $b] = sscanf(substr($hex, 0, 7), '#%02x%02x%02x');
// Яскравість Rec. 709, діапазон 0-255.
return (0.2126 * $r + 0.7152 * $g + 0.0722 * $b) > 140;
}
}
<figcaption class="{{ Color::isLight($photo->dominant_color) ? 'text-gray-900' : 'text-white' }}">
{{ $photo->caption }}
</figcaption>
# Заповнення полів при масштабуванні та повороті
Це ж визначення кольору можна використовувати прямо під час трансформацій. Передача аргументу dominant у метод contain() заповнить пусті поля кольором самого зображення замість фіксованого значення:
Image::fromUpload($request->file('photo'))
->contain(1200, 800, background: 'dominant')
->store('photos');
Вертикальне фото в горизонтальному контейнері отримає поля, що пасують до нього за тональністю — це виглядає набагато краще в сітці, ніж стандартні чорні чи білі смуги. Метод rotate() працює аналогічно для кутів, що з’являються при повороті:
$image->rotate(8, background: 'dominant');
# Чим цей метод не є
dominantColor() повертає лише один усереднений колір. Це не екстрактор палітри, він не знайде акцентний колір, який би обрав дизайнер. Якщо вам потрібен набір характерних зразків або кластеризація, яка ігнорує фон, краще використовувати спеціалізовані бібліотеки.
Але для створення заповнювачів, фону для полів та стилізації карток під колір контенту — одного середнього кольору цілком достатньо. І тепер це частина фреймворку.
# Додаткові матеріали
- Практичний посібник з обробки зображень у Laravel охоплює решту API: зміну розміру, кроп, конвертацію форматів та зберігання.
- Нотатки до релізу Laravel 13.24: підтримка домінантного кольору та HEIC.
- Функцію додав @dr-codswallop у PR #60932.