Минулого тижня я закінчив сайт для запуску нового бізнесу. Дизайн був досить простим: одна сторінка з різними розділами, представленими горизонтальними блоками в повну ширину з різним кольором фону і з логотипом фіксованим у верхньому лівому куті.
Єдина особливість полягала у тому, що логотип повинен змінювати кольори, під час прокручування сторінку користувачем, в залежності від того, який розділ він перекриває. При розміщенні над секцією з темним фоном, логотип повинен бути білим. А при перекритті розділу зі світлим фоном, він повинен бути темно-синього кольору.
Першим, що спало на думку, було використати два малюнки й замінювати їх, тоді, коли користувач прокручує сторінку, але тут такий підхід не працює. Погляньмо уважніше на дизайн сторінки вище. Другий екран показує, що логотип перекриває секцію, яка є частково темною і світлою. Коли це відбувається, частини логотипу, які покривають кожну з секцій повинні бути забарвлені відповідно.
Спроба #1: анімація позиціювання
Після деяких пошуків, я натрапив на цей приклад. Він працює, маючи основну версію логотипу, з position: fixed
, і кілька копій – по одній для кожного розділу – з position: absolute
. Під час прокручування, позиція кожної з копій змінюється таким чином, що ідеально перекриває основний логотип і створює ілюзію того, що це один і той самий елемент.
Під час тестів, усе чудово працювало у Chrome, але зображення мерехтіло у Safari.
Спроба #2: SVG clip-path
Коли спроба #1 не вдалась, я намагався підійти до справи з іншого боку. Замість того, щоб створювати різні копії елементу для різних секцій і анімувати їхні позиції, я створив дві копії (одну темну й одну світлу) логотипу і розмістив їх точно одну над одною з фіксованою позицією для кожної.
Далі, потрібно було знайти спосіб приховувати частину верхнього логотипа, таким чином, щоб утворена пустота відкривала відповідну частину нижнього. Це було схожим на маску, тому використати clip-path
було хорошим рішенням. При використанні його на елементах HTML, його підтримка обмежена, але мій логотип був у SVG форматі, у цьому випадку, підтримка розповсюджена.
Створення масок
Я почав зі створення SVG <clip-path>
елементів для кожного логотипу.
<!-- Основний логотип -->
<svg>
<defs>
<clipPath id="logo-main-mask">
<rect x="0" y="0" width="200" height=[](http://)"120" />
</clipPath>
</defs>
<g clip-path="url(#logo-main-mask)">
<use xlink:href="#logo"/>
</g>
</svg>
<!-- Копія логотипу -->
<svg>
<defs>
<clipPath id="logo-alt-mask">
<rect x="0" y="120" width="200" height="0" />
</clipPath>
</defs>
<g clip-path="url(#logo-alt-mask)">
<use xlink:href="#logo"/>
</g>
</svg>
Тоді я створив функцію, яка дозволяла мені маніпулювати розмірами та розташуванням масок таким чином, що була б відображена точна частина кожного логотипу. Ця функція отримує два аргументи:
-
amount
– кількість пікселів верхнього логотипа, які потрібно відобразити -
isDark
– булівське значення, що визначає чи перша секція, яку перекриває логотип, має темний фон
// `logoDimensions` об'єкт, який містить розміри
// основного логотипу
function blendLogoMasks(amount, isDark) {
if ((maskCache.isDark === isDark) && (maskCache.amount === amount)) {
return
}
var alt = {}
var main = {}
if (isDark) {
alt.y = amount + 1
alt.height = logoDimensions.height - amount
main.y = 0
main.height = amount
} else {
alt.y = 0
alt.height = amount
main.y = amount + 1
main.height = logoDimensions.height - amount
}
$('#logo-alt-mask rect').attr({
y: alt.y,
height: alt.height
})
$('#logo-main-mask rect').attr({
y: main.y,
height: main.height
})
maskCache.isDark = isDark
maskCache.amount = amount
}
// Це згенерує логотип з 100px білого кольору
// і рештою темного кольору
blendLogoMasks(100, false)
Декілька речей для замітки:
- У прикладах використано jQuery, для більш лаконічного відображення маніпуляцій з DOM, але це абсолютно не суттєво для створення такої функціональності;
-
maskCache
– об'єкт, який зберігає два останніх аргументи (amount
таisDark
), з якими була викликана функція уникаючи звернення до DOM під час багаторазових викликів функції з тими самими значеннями. Це оптимізація продуктивності, яка далі буде більш зрозумілою.
Відображення розділів
Щоб більш легко розрахувати, коли логотип перекриває кожен розділ, я позначив кожну секцію як об'єкт, що містить: позицію, в якій закінчується секція, наявність світлого чи темного фону та посилання на DOM вузол.
function generateSectionsMap() {
var sections = []
$sections.each(function () {
var top = $(this).offset().top
sections.push({
$el: this,
end: top + $(this).outerHeight(),
isDark: $(this).hasClass('js-section-dark')
})
})
return sections
}
Оновлення суміші масок під час прокручування
Нарешті, нам потрібно оновити суміш масок, коли користувач прокручує сторінку. Для цього я спочатку створив функцію, яка визначає, коли логотип перекриває один або два розділи, а також кольори їхніх фонів.
function updateLogo(sections) {
var scrollOffset = $body.scrollTop()
var logoStart = $logoMain.offset().top
var logoEnd = logoStart + logoDimensions.height
var section
$.each(sections, function (index, section) {
if (section.end >= logoStart) {
if (
section.end <= logoEnd &&
sections[index + 1] &&
sections[index + 1].isDark !== section.isDark
) {
// Логотип знаходиться на межі розділів, перший елемент
// суміші масок отримує ту кількість пікселів розділу,
// яка не знаходиться під логотипом. Ми це робимо тільки тоді, overlapping the logo.
// коли розділи мають різний фон, інакше приймаємо це як один розділ.
blendLogoMasks(section.isDark, section.end - logoStart)
} else {
// Логотип - на одному розділі, перший елемент у суміші масок приймає всю висоту логотипу.
blendLogoMasks(section.isDark, logoDimensions.height)
}
return false
}
})
}
У кінці, нам просто потрібно прикріпити цю функцію до події onScroll
, а також викликати її, коли сторінка завантажується.
var sections = generateSectionsMap()
updateLogo(sections)
$(window).on('scroll', function () {
updateLogo(sections)
})
І це працює!
Кінцевий результат
Ось як це виглядає на тій самій версії Safari, яка раніше була проблематичною.
Поглянути й проаналізувати приклад можна тут.
Ще немає коментарів