Техніка попереднього розмиття фонового зображення під час завантаження

22

Від автора: у даній статті ми розглянемо техніку попереднього розмиття фонового зображення під час його завантаження. Вже протягом довгого часу нам доступні CSS фільтри. Разом з режимами накладення за допомогою фільтрів нам відкриваються абсолютно нові можливості по створенню і маніпулювання об’єктами, які раніше були можливі тільки в Photoshop е. В даній статті Еміль розповідає про високопродуктивну техніку, в якій використовуються вже забуті фільтри – функція фільтрів – а також про метод з використанням SVG.

Все почалося з поста від команди розробників офіційного додатку Facebook і того, як вони завантажували превью фотографій. Проблема з цими фото була в тому, що вони були величезні, і часто завантаження займала досить довгий час. А користувач, коли бачив, як монотонний фон змінюється на фотографію під час завантаження, отримував не самий ідеальний досвід роботи з додатком.

Дана проблема стоїть особливо гостро на низькошвидкісних або мобільних з’єднаннях. Перед вами з’являється просто порожній сірий прямокутник, і ви чекаєте, коли завантажиться зображення.

В ідеалі зображення має кодуватися ще в першій відповіді від API після отримання інформації про профілі. Щоб зображення помістилося в запиті, необхідно, щоб його вага не перевищувала 200 байт. Проблематично, тим більше, що фото важить 100 Кбайт. Так яким чином домогтися того, щоб зображення важив 200 байт, і як хоч що-небудь показати користувачеві до повного завантаження фотографії?

Досить оригінальним рішенням буде повернути крихітне зображення (приблизно 40 пікселів в ширину) і розтягнути його на весь контейнер з розмиття за Гаусом. Таким чином, користувач відразу бачить більш менш прийнятний фоновий малюнок і має уявлення про те, що з себе представляє даний зображення. Початкове зображення можна завантажити у фоновому режимі і плавно замінити. Розумно! Кілька переваг даного методу:

Завантаження сприймається трохи швидше

Використовуються традиційні методи підвищення продуктивності.

Метод повністю працює через браузер.

Даний метод нагоді при завантаженні великих фонових зображень в хедері. Можна не завантажувати важкі зображення, але іноді необхідно шукати компроміс в догоду користувачеві. Найкраще, що можна зробити в даній ситуації, це оптимізувати сприйняття продуктивності. Так що можете взяти даний метод собі на озброєння.

Робочий приклад

Ми переробимо наш метод і скористаємося для цього критичним CSS. Самий перший запит буде завантажувати маленьке зображення і вбудовувати CSS код безпосередньо в HTML сторінку, і лише після попереднього промальовування розмитого зображення буде відображатися зображення високої якості. Коли завантаження завершена, це буде виглядати приблизно так:

Техніка попереднього розмиття фонового зображення під час завантаження

У цьому прикладі зображення використовується як прикраси, а не в якості контенту. Існують свої тонкощі в тому випадку, якщо зображення йде як контент (тобто використовується тег img) або як просто фонове зображення. Найпоширенішим рішенням у разі, якщо зображення використовується в якості фонового малюнка, буде техніка розумного зміни розміру (як CSS значення cover і contain). У випадку, коли зображення використовується в якості контенту, є нові властивості типу object-fit, що полегшують цей підхід. На сайті Medium вже використовується метод розмитих зображень для підвищення швидкості завантаження, однак про корисність даного методу ведуться суперечки – як даний метод відреагує, якщо щось піде не так? Як би те ні було, у цій статті ми зосередимося на методі для фонових зображеннях. Принцип роботи:

Всередині тега style знаходиться инлайновое маленьке зображення (40х22рх), закодоване за допомогою кодування base64. Також всередині тега style прописуються основні стилі і правила для розмиття по Гауссу для фонового зображення. В іншому класі описуються стилі для великого зображення в хедері сайту.

Через инлайновый CSS код виходить URL великого зображення, і воно завантажується через JavaScript. Якщо з якихось причин якісне зображення не завантажилося, не страшно – розмите зображення все ще на місці, виглядає непогано.

Після завантаження великої зображення додається спеціальний клас, і розмите зображення перетворюється на звичайне. Видалення розмитого зображення можна анімувати.

Фінальний приклад можна подивитися за посиланням. Перш ніж завантажиться основне зображення високої якості, ми бачимо розмитий його варіант. Якщо у вас не так, перезавантажте сторінку і очистіть кеш.

Маленьке оптимізоване зображення

Насамперед необхідно створити превью зображення. Facebook домігся ваги зображення в 200 байт з допомогою стиснення (один із прикладів це зберігання неизменяющихся бітів JPEG зображення з хедера в самому додатку), але ми не будемо настільки заглиблюватися. Якщо це зображення 40 на 22 пікселя пропустити через спеціальне програмне забезпечення по оптимізації зображень, його вага зменшиться до 1000 байт.

Техніка попереднього розмиття фонового зображення під час завантаження

Велике зображення 1500 × 823 пікселів важить приблизно 120 Кбайт. Розмір файла може бути і менше, але ми залишимо такий. У реальному житті легше було б, напевно, завантажувати зображення різного розміру в залежності від розміру вікна браузера, або взагалі завантажувати інший формат типу WebP.

Функція filter

Наступний етап це збільшити маленьке зображення до розмірів контейнера таким чином, щоб воно виглядало прийнятно. Тут нам і допоможе функція filter(). Тема фільтрів CSS досить заплутана, існує три типу: властивість filter, backdrop-filter (специфікація Filter Effects Level 2 spec) і функція filter() для зображень. Розглянемо спочатку властивість:

.myThing {
filter: hue-rotate(45deg);
}

Можна застосувати кілька фільтрів, кожний з яких працює з результатом попереднього – як з трансформаціями. Існує цілий набір заздалегідь заданих фільтрів, які можна використовувати: blur(), brightness(), contrast(), drop-shadow(), grayscale(), hue-rotate(), invert(), opacity(), sepia() і saturate().

Радує той факт, що загальна специфікація як для CSS, так і для SVG. Тобто можна використовувати не тільки заздалегідь описані фільтри, але також можна створити власні в SVG а потім прописати їх в CSS.

.myThing {
filter: url(myfilter.svg#myCustomFilter);
}

При використанні backdrop-filter буде точно такий же ефект, даний метод застосовується при суміщенні прозорого елемента з його фоном – кращий спосіб для створення ефекту «замороженого скла».

І нарешті, функція filter(). Ідея в тому, щоб скрізь, де вказані посилання на зображення, спочатку пропустити ці зображення через набір фільтрів. Наше маленьке зображення хедера ми можна закодувати за допомогою base64 dataURI і пропустимо через фільтр blur().

.post-header {
background-image: filter(url(data:image/jpeg;base64,/9j/4AAQ …[truncated] …), blur(20px));
}

Відмінно, саме те, чого ми хотіли досягти при имитировании техніки додатка Facebook. Однак виникають проблеми з підтримкою. Властивість filter підтримується в останніх версіях всіх браузерів крім IE, але ні один з браузерів крім WebKit не підтримує функцію filter().

Під WebKit я маю на увазі нічні збірки WebKit браузерів крім Safari на момент написання статті. Функція filter() чисто технічно працює в iOS9, якщо додати вендорные префікс –webkit-filter(), але офіційно про це ніде не повідомлялося, що трохи дивно. Причина може бути у наявності жахливого бага властивість background-size: вихідне зображення не змінює свого розміру, а ось кінцеве з фільтром змінює. Цей баг ламає всю роботу з фоновими зображеннями, особливо з розмиттям фонових зображень. Баг був виправлений, але не до виходу Safari 9, так що, я думаю, вони не стали анонсувати цю властивість.

А що, власне, робити нам з непрацюючою функцією filter()? В браузерах без підтримки даної функції можна поставити звичайний суцільний фон на час завантаження зображення. Але у випадку якщо JS не завантажився, то фон взагалі не завантажиться.

Ми не викинемо функцію filter(), замість того, щоб емулювати функцію фільтра до початкового зображення з допомогою SVG, ми використовуємо її для анімації переходу від розмитого зображення чітким.

Створюємо ефект розмиття за допомогою SVG

У специфікації є SVG еквівалент для фільтра blur(), за допомогою пари хитрощів можна переробити ефект розмиття в SVG:

При використанні розмиття по Гауссу краю зображення стають трохи прозорими. Виправити це можна за допомогою фільтра feComponentTransfer. Цей компонент дозволяє маніпулювати кожним кольоровим каналом (в тому числі і альфа) вихідного зображення. Даний спосіб використовує feFuncA елемент, який замінює будь-які значення між 0 і 1 в альфа-каналі на 1, що означає повну непрозорість зображення.

Атрибут color-interpolation-filters тега filter повинен мати значення sRGB. SVG фільтри за замовчуванням працюють із простором linearRGB, а CSS з sRGB. В більшості браузерів колірна корекція працює правильно, але в Safari / WebKit браузерах без цього значення всі кольори відображаються, як вицвілі.

Атрибуту filterUnits задається значення userSpaceOnUse. Простою мовою, це означає, що координати і прямі (як stdDeviation для розмиття) націлені на конкретні пікселі зображення, яке необхідно розмити.

В результаті ми отримуємо такий SVG код:

Властивості filter використовується вбудована функція url(), у якій можна вказати посилання, так і закодований адреса SVG фільтра. Так як же застосувати фільтр до вмісту властивості background-image: url(…)?

SVG файли можуть посилатися на інші зображення, ми можемо застосувати фільтри до цих зображень через SVG. Проблема в тому, що фонові зображення на SVG не можуть використовувати сторонні ресурси. Але це можна обійти, закодований JPG зображення всередині SVG за допомогою base64. Для перегляду великого зображення це зробити не можна, а ось для нашого маленького цілком можливо. Код SVG буде приблизно такий:

Черговий недолік (порівняно з функцією filter() для растрового зображення) в тому, що щоб правильно працювати з фоновим зображенням, нам необхідно вручну встановити розміри для SVG. Для підтримання співвідношення сторін в SVG є значення viewBox. Щоб бути впевненим у кросбраузерності даного методу, властивостей width і height задаються відповідні з вимірюваннями значення (наприклад, в IE не зберігається співвідношення сторін, якщо такі властивості не задані). І нарешті, image елемент розтягується на все полотно SVG.

Тепер можна використовувати цей файл в якості фонового зображення в шапці сайту, це буде виглядати приблизно так:

Техніка попереднього розмиття фонового зображення під час завантаження

Щоб уникнути додаткових запитів можна помісити блок-обгортку SVG прямо в CSS. Инлайновые стилі SVG потрібні для кодування URI, я використовував SVG encoder від yoksel. Тепер у нас один dataURI містить інший dataURI. Як у фільмі Початок!

Після кодування SVG url(), необхідно вставити результат. Варто зауважити, що даний метод спрацював, попередньо необхідно додати деякі метадані: data:image/svg+xml;charset=utf-8. Кодування charset вкрай важлива: у разі правильної кодування закодований SVG буде працювати у всіх браузерах.

.post-header {
background-color: #567DA7;
background-size: cover;
background-image: url(data:image/svg+xml;charset=utf-8%3Csvg…);
}

На даний момент, якщо використовувати GZIP, то вся сторінка разом із зображенням важить 5Кб і на ній всього один запит.

Отримуємо URL великого зображення

Тепер необхідно створити правило для в хедері там, де ми будемо замінювати розмите зображення на зображення високої якості.

.post-header-enhanced {
background-image: url(largeimg.jpg);
}

Замість того, щоб просто поміняти класи, тим самим викликаючи перехід до великого зображення, ми завантажимо велике зображення у фоновому режимі і тільки потім застосуємо клас. Саме таким чином, якщо головне зображення завантажилося, ми зможемо плавно переключити одне зображення на інше. Щоб не ускладнювати URL картинки як в CSS, так і в JS, ми просто витягнемо за допомогою JavaScript URL прямо зі стилів. Так як клас ще не застосований, то ми можемо звертатися до фонового зображення за допомогою headerElement.style.backgroundImage – стилі ще не знають про фоновому зображенні. Щоб вирішити дану проблему ми скористаємося CSSOM – the CSS Object Model і вважаємо тільки ті JS властивості, за допомогою яких можна переміщатися по CSS правилами.

Код нижче шукає клас із зображенням високої якості і за допомогою регулярного виразу витягає з нього адресу. Після, зображення завантажується в фоновому режимі, і як тільки воно завантажилося спрацьовує зміна класу.

window.onload = function loadStuff() {
var win, doc, img, header, enhancedClass;
// якщо старі браузери, то відразу виходимо (тобто IE 8).
if (!(‘addEventListener.’ in window)) {
return;
}
win = window;
doc = win.document;
img = new Image();
header = doc.querySelector(‘.post-header’);
enhancedClass = ‘post-header-enhanced’;
// Знаходимо перше згадування про адресу фонового зображення високої //якості, навіть якщо стилі не застосовані.
var bigSrc = (function () {
// Знаходимо всі об’єкти CssRule всередині инлайновых стилів
var styles = doc.querySelector(‘style’).sheet.cssRules;
// зберігаємо оголошення background-image…
var bgDecl = (function () {
// …за допомогою рекурсивної функції з циклом
var bgStyle, i, l = styles.length;
for (i=0; iСкрипт відразу завершується, якщо addEventListener. не підтримується. З іншими браузерами даний метод повинен чудово працювати. Наскільки я знаю, всі сучасні браузери з підтримкою SVG також підтримують і CSSOM з функціями JavaScript, які використовувалися вище коді.

Анимируем момент перемикання

Після того, як ми дізналися про функції filter(), ми не стали її відразу застосовувати. А зараз нам необхідно додати анімований перехід від розмитого зображення чітким. На даний момент спосіб працює тільки в нічних збірках webkit браузерів, можна сміливо використовувати @supports. Нижче GIF приклад ефекту:

Техніка попереднього розмиття фонового зображення під час завантаження

Зверніть увагу на те, що ми не можемо використовувати властивість transition: функція filter() піддається анімації, але тільки для змінних значень – у випадку з фоновим зображенням даний спосіб не працює. Тим не менш, можна скористатися анімацією, але у такому разі нам доведеться скопіювати URL зображення ще два рази, як початкове і кінцеве значення. Невелика жертва. Нижче представлені стилі для чіткого зображення хедера для браузерів, що підтримують функцію filter():

@supports (background-image: filter(url(‘i.jpg’), blur(1px))) {
.post-header {
transform: translateZ(0);
}
.post-header-enhanced {
animation: sharpen .5s both;
}
@keyframes sharpen {
from {
background-image: filter(largeimg.jpg), blur(20px));
}
to {
background-image: filter(largeimg.jpg), blur(0px));
}
}
}

Остання деталь з translateZ(0) – трюк для хедера: без цього рядка анімація сильно смикається. Я хотів би використовувати найсучасніші властивості типу will-change: background-image, однак браузер не захотів створювати окремий апаратний шар, так що довелося скористатися старим трюком з нульовою 3D трансформацією.

Швидке і прогресивне фонове зображення

Ми отримали те, що хотіли – сторінка з величезним фоновим зображенням (хоча і розмитим) важить 5 Кб і лінива завантаження зображення високої якості. На даний момент анімація переходу від розмитого зображення до чіткого підтримується тільки в webkit браузерах. Але я сподіваюся, що незабаром в інших браузерах почне нарешті працювати функція filter(). Впевнений, існує безліч цікавих способів, в яких дана функція придалася б.